diff --git a/apps/back-office/src/app/components/add-form-modal/add-form-modal.component.html b/apps/back-office/src/app/components/add-form-modal/add-form-modal.component.html index ee7d572aad..c33f6b7c66 100644 --- a/apps/back-office/src/app/components/add-form-modal/add-form-modal.component.html +++ b/apps/back-office/src/app/components/add-form-modal/add-form-modal.component.html @@ -84,9 +84,7 @@

{{ 'models.form.new' | translate }}

- + import('@oort-front/shared').then((m) => m.ProfileViewModule), }, + { + path: 'datastudio', + loadChildren: () => + import('./pages/data-studio/data-studio.module').then( + (m) => m.DataStudioModule + ), + }, { path: 'referencedata', children: [ diff --git a/apps/back-office/src/app/dashboard/dashboard.component.ts b/apps/back-office/src/app/dashboard/dashboard.component.ts index 495336d34c..2ae04a537d 100644 --- a/apps/back-office/src/app/dashboard/dashboard.component.ts +++ b/apps/back-office/src/app/dashboard/dashboard.component.ts @@ -78,6 +78,13 @@ export class DashboardComponent extends UnsubscribeComponent { icon: 'cloud_download', }); } + if (this.ability.can('read', 'Resource')) { + dataItems.push({ + name: this.translate.instant('common.dataStudio.few'), + path: '/datastudio', + icon: 'build_circle', + }); + } if (dataItems.length > 0) { navGroups.push({ name: this.translate.instant('pages.formBuilder.title'), diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields-type-mapping.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields-type-mapping.ts new file mode 100644 index 0000000000..e423734cfa --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields-type-mapping.ts @@ -0,0 +1,144 @@ +/** Interface for the data generation map */ +interface DataGenerationMap { + [key: string]: { + displayName: string; + source: string; + options?: any[] | null; + }; +} + +/** Data generation map */ +export const dataGenerationMap: DataGenerationMap = { + boolean: { + displayName: 'Yes/No (Boolean)', + source: 'common.dataStudio.dataGeneration.map.boolean', + }, + checkbox: { + displayName: 'Checkboxes', + source: 'common.dataStudio.dataGeneration.map.checkbox', + }, + color: { + displayName: 'Color', + source: 'common.dataStudio.dataGeneration.map.color', + }, + comment: { + displayName: 'Comment', + source: 'common.dataStudio.dataGeneration.map.text', + }, + date: { + displayName: 'Date', + source: 'common.dataStudio.dataGeneration.map.date', + }, + 'datetime-local': { + displayName: 'Date and Time', + source: 'common.dataStudio.dataGeneration.map.datetimelocal', + }, + dropdown: { + displayName: 'Dropdown', + source: 'common.dataStudio.dataGeneration.map.dropdown', + }, + email: { + displayName: 'Email', + source: 'common.dataStudio.dataGeneration.map.email', + }, + expression: { + displayName: 'Expression (read-only)', + source: 'common.dataStudio.dataGeneration.map.expression', + }, + file: { + displayName: 'File Upload', + source: ' ', + }, + geospatial: { + displayName: 'Geospatial', + source: 'common.dataStudio.dataGeneration.map.geospatial', + }, + html: { + displayName: 'HTML', + source: ' ', + }, + image: { + displayName: 'Image', + source: ' ', + }, + matrix: { + displayName: 'Single-Select Matrix', + source: 'common.dataStudio.dataGeneration.map.matrix', + }, + matrixdynamic: { + displayName: 'Dynamic Matrix', + source: 'common.dataStudio.dataGeneration.map.matrixdynamic', + }, + matrixdropdown: { + displayName: 'Multi-Select Matrix', + source: 'common.dataStudio.dataGeneration.map.matrixdropdown', + }, + month: { + displayName: 'Month', + source: 'common.dataStudio.dataGeneration.map.month', + }, + multipletext: { + displayName: 'Multiple Text', + source: 'common.dataStudio.dataGeneration.map.multipletext', + }, + number: { + displayName: 'Number', + source: 'common.dataStudio.dataGeneration.map.numeric', + }, + owner: { + displayName: 'Owner', + source: 'common.dataStudio.dataGeneration.map.owner', + }, + paneldynamic: { + displayName: 'Panel Dynamic', + source: 'common.dataStudio.dataGeneration.map.paneldynamic', + }, + password: { + displayName: 'Password', + source: 'common.dataStudio.dataGeneration.map.password', + }, + radiogroup: { + displayName: 'Radio Button Group', + source: 'common.dataStudio.dataGeneration.map.radiogroup', + }, + range: { + displayName: 'Range', + source: 'common.dataStudio.dataGeneration.map.range', + }, + resource: { + displayName: 'Resource', + source: 'common.dataStudio.dataGeneration.map.resource', + }, + resources: { + displayName: 'Resources', + source: 'common.dataStudio.dataGeneration.map.resources', + }, + tagbox: { + displayName: 'Multi-Select Dropdown', + source: 'common.dataStudio.dataGeneration.map.tagbox', + }, + tel: { + displayName: 'Phone Number', + source: 'common.dataStudio.dataGeneration.map.tel', + }, + text: { + displayName: 'Text', + source: 'common.dataStudio.dataGeneration.map.text', + }, + time: { + displayName: 'Time', + source: 'common.dataStudio.dataGeneration.map.time', + }, + url: { + displayName: 'URL', + source: 'common.dataStudio.dataGeneration.map.url', + }, + users: { + displayName: 'Users', + source: 'common.dataStudio.dataGeneration.map.users', + }, + week: { + displayName: 'Week', + source: 'common.dataStudio.dataGeneration.map.week', + }, +}; diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.html b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.html new file mode 100644 index 0000000000..b2720c8dd0 --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.html @@ -0,0 +1,217 @@ + +
+
+

{{ 'common.field.few' | translate }}

+ + + + {{ 'common.dataStudio.dataGeneration.selectAll' | translate }} + + +
+ + +
+
+ + + {{ field.name }} + + + +
+
+
+ {{ + 'common.dataStudio.dataGeneration.fieldType' | translate + }}{{ getDisplayName(field) }} +
+
+ {{ + 'common.dataStudio.dataGeneration.generationSource' + | translate + }} +
+ {{ getGenerationSource(field) }} +
+
+
+ +
+ + + {{ + 'common.dataStudio.dataGeneration.setDefault' | translate + }} + + + +
+ + + +
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ {{ + 'common.dataStudio.dataGeneration.fieldType' | translate + }}{{ getDisplayName(field.type) }} +
+
+ {{ + 'common.dataStudio.dataGeneration.generationNotPossible' + | translate + }} +
+
+
+
+
+
+ +
+ + + {{ 'common.dataStudio.dataGeneration.generate' | translate }} + +
+
+
+
diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.scss b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.scss new file mode 100644 index 0000000000..39a4668069 --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.scss @@ -0,0 +1,7 @@ +::ng-deep .accordion-item span { + @apply flex w-full; +} + +.field-type { + border-right: 1px solid #e0e0e0; +} diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.spec.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.spec.ts new file mode 100644 index 0000000000..d0e75a24ed --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataGenerationFieldsComponent } from './data-generation-fields.component'; + +describe('DataGenerationFieldsComponent', () => { + let component: DataGenerationFieldsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DataGenerationFieldsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DataGenerationFieldsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.ts new file mode 100644 index 0000000000..03b5461544 --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/data-generation-fields.component.ts @@ -0,0 +1,285 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { FormQueryResponse, UnsubscribeComponent } from '@oort-front/shared'; +import { dataGenerationMap } from './data-generation-fields-type-mapping'; +import { + FormBuilder, + FormGroup, + FormControl, + Validators, + FormArray, +} from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { Apollo, QueryRef } from 'apollo-angular'; +import { GET_FORM_STRUCTURE } from './graphql/queries'; +import { GENERATE_RECORDS, EDIT_RECORD } from './graphql/mutations'; +import { Model } from 'survey-core'; +import { FormBuilderService } from './../../../../../../../../libs/shared/src/lib/services/form-builder/form-builder.service'; +import { takeUntil, firstValueFrom } from 'rxjs'; +import { indexOf } from 'lodash'; +import { + EditRecordMutationResponse, + GenerateRecordsMutationResponse, +} from './../../../../../../../../libs/shared/src/lib/models/record.model'; +import { SnackbarService } from '@oort-front/ui'; +const TIMEOUT_DURATION = 1000; +/** Conversion fields component */ +@Component({ + selector: 'app-data-generation-fields', + templateUrl: './data-generation-fields.component.html', + styleUrls: ['./data-generation-fields.component.scss'], +}) +/** Data generation class component */ +export class DataGenerationFieldsComponent + extends UnsubscribeComponent + implements OnChanges +{ + /** Form id input */ + @Input() formId!: string; + + /** Form */ + private formStructureQuery!: QueryRef; + private form: any = {}; + public dataGenerationForm!: FormGroup; + public survey: Model = new Model(); + + /** Form fields */ + public fields: any[] = []; + + /** Flags */ + public loading = false; + public isChecked = false; + public accordionItemExpanded: number = -1; + + /** + * Data generation component constructor + * + * @param apollo Apollo client service + */ + constructor( + private apollo: Apollo, + private fb: FormBuilder, + private translate: TranslateService, + private formBuilderService: FormBuilderService, + private snackBar: SnackbarService + ) { + super(); + } + + /** On changes hook */ + ngOnChanges() { + this.loading = true; + if (this.formId) { + this.formStructureQuery = this.apollo.watchQuery({ + query: GET_FORM_STRUCTURE, + variables: { + id: this.formId, + }, + }); + this.formStructureQuery.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(({ data, loading }) => { + this.dataGenerationForm = this.createDataGenerationForm(); + this.isChecked = false; + this.fields = + JSON.parse(data.form.structure ?? '') + .pages.reduce( + (acc: any, page: any) => acc.concat(page.elements), + [] + ) + ?.filter((field: any) => !field.generated) ?? []; + this.fields.forEach((field: any) => { + this.fieldsForm.push(this.createFieldForm()); + this.fieldsForm.controls[indexOf(this.fields, field)].patchValue({ + field: field.name, + setDefault: false, + include: false, + //option: dataGenerationMap[field.type].options?.[0].action, + }); + }); + this.form = data.form; + this.loading = loading; + }); + } else { + this.loading = false; + } + } + + /** + * Get the display name in the conversion map + * + * @param field the Field + * @returns The type display name + */ + public getDisplayName(field: any): string { + if (!dataGenerationMap[field.type] && !dataGenerationMap[field.inputType]) { + return field.type; + } + if (dataGenerationMap[field.inputType]) { + return dataGenerationMap[field.inputType].displayName; + } + return dataGenerationMap[field.type].displayName; + } + + /** + * Get generation method to display + * + * @param field the Field + * @returns Generation source for the type + */ + public getGenerationSource(field: any): string { + if (!dataGenerationMap[field.type] && !dataGenerationMap[field.inputType]) { + return ''; + } + if (dataGenerationMap[field.inputType]) { + return ( + this.translate.instant(dataGenerationMap[field.inputType].source) ?? '' + ); + } + return this.translate.instant(dataGenerationMap[field.type].source) ?? ''; + } + + /** + * onClick event for the button + */ + public onClick() { + this.generateData(); + } + + /** + * Accordion item open handler + * + * @param index Item index + */ + public onAccordionItemOpen(index: number) { + this.survey = this.formBuilderService.createSurvey( + this.getSingleFieldSurveyStructure(this.fields[index].name) + ); + // Resetting the "default" value on survey in case its closed and opened again + if (this.fieldsForm.controls[index].get('setDefault')?.value) { + this.survey.setValue( + this.fields[index].name, + this.fieldsForm.controls[index].get('default')?.value + ); + } + // Subscribe the fieldsForm "default" control to the survey value change + this.survey.onValueChanged.add((sender, options) => { + this.fieldsForm.controls[index].patchValue({ + default: options.value, + }); + }); + } + + /** + * Create the dataGeneration form + * + * @returns the dataGeneration form + */ + private createDataGenerationForm() { + return this.fb.group({ + fieldsForm: this.fb.array([]), + recordsNumber: new FormControl(null, Validators.required), + }); + } + + /** + * Create field form which is going to be a formArray + * + * @returns the fieldForm + */ + private createFieldForm() { + return this.fb.group({ + field: new FormControl(null, Validators.required), + include: new FormControl(false, Validators.required), + setDefault: new FormControl(false, Validators.required), + default: new FormControl(), + minDate: new FormControl(), + maxDate: new FormControl(), + minNumber: new FormControl(), + maxNumber: new FormControl(), + minTime: new FormControl(), + maxTime: new FormControl(), + }); + } + + /** + * Recursive function to get a field structure from a nested JSON and return a single field survey structure + * + * @param fieldName Field name + * @returns The survey structure + */ + private getSingleFieldSurveyStructure(fieldName: string): any { + // Parses the structure, concatenates all fields from "elements" from all pages and finds the field by name + const resultStructure = JSON.parse(this.form.structure) + .pages.reduce((acc: any, page: any) => acc.concat(page.elements), []) + .find((obj: any) => obj.name === fieldName); + + // Returns a simple survey structure with one field + return { + pages: [ + { + name: 'page1', + elements: [resultStructure], + }, + ], + showQuestionNumbers: 'off', + }; + } + + /** + * Generate new record data + */ + private async generateData(): Promise { + this.loading = true; + const res = await firstValueFrom( + this.apollo.mutate({ + mutation: GENERATE_RECORDS, + variables: { + form: this.formId, + data: this.dataGenerationForm.value, + }, + }) + ); + const expressionSurvey = this.formBuilderService.createSurvey( + this.form.structure + ); + for (const record of res.data?.generateRecords ?? []) { + expressionSurvey.data = record.data; + await new Promise((resolve) => setTimeout(resolve, TIMEOUT_DURATION)); + await firstValueFrom( + this.apollo.mutate({ + mutation: EDIT_RECORD, + variables: { + id: record.id, + data: expressionSurvey.data, + }, + }) + ); + } + this.loading = false; + this.snackBar.openSnackBar( + this.dataGenerationForm.value.recordsNumber + + ' ' + + this.translate.instant('common.notifications.dataGenerated') + ); + } + + /** Getter for the fieldsForm */ + get fieldsForm() { + return this.dataGenerationForm.get('fieldsForm') as FormArray; + } + + /** + * Function to handle Select all checkbox + */ + public selectAll() { + if (this.isChecked) { + this.fieldsForm.controls.forEach((control) => { + control.patchValue({ include: true }); + }); + } else { + this.fieldsForm.controls.forEach((control) => { + control.patchValue({ include: false }); + }); + } + } +} diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/graphql/mutations.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/graphql/mutations.ts new file mode 100644 index 0000000000..a26be18b3c --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/graphql/mutations.ts @@ -0,0 +1,33 @@ +import { gql } from 'apollo-angular'; + +/** Graphql query for generating records */ +export const GENERATE_RECORDS = gql` + mutation generateRecords($form: ID!, $data: JSON!) { + generateRecords(form: $form, data: $data) { + id + createdAt + modifiedAt + data + } + } +`; + +export const EDIT_RECORD = gql` + mutation editRecord( + $id: ID! + $data: JSON + $version: ID + $template: ID + $display: Boolean + ) { + editRecord(id: $id, data: $data, version: $version, template: $template) { + id + data(display: $display) + createdAt + modifiedAt + createdBy { + name + } + } + } +`; diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/graphql/queries.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/graphql/queries.ts new file mode 100644 index 0000000000..5134b0f4fa --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-generation-fields/graphql/queries.ts @@ -0,0 +1,58 @@ +import { gql } from 'apollo-angular'; + +// === GET RECORD BY ID === +/** Graphql request for getting a record by its id */ +export const GET_RECORD_BY_ID = gql` + query GetRecordById($id: ID!) { + record(id: $id) { + id + incrementalId + createdAt + modifiedAt + createdBy { + name + } + modifiedBy { + name + } + data + form { + id + name + queryName + structure + fields + metadata { + name + automated + canSee + canUpdate + } + } + } + } +`; + +// === GET FORM STRUCTURE === + +/** Graphql request for getting the structure of a form by its id */ +export const GET_FORM_STRUCTURE = gql` + query GetFormStructure($id: ID!) { + form(id: $id) { + id + name + structure + fields + metadata { + name + automated + canSee + canUpdate + } + resource { + id + name + } + } + } +`; diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-studio-routing.module.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio-routing.module.ts new file mode 100644 index 0000000000..37f1c98185 --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio-routing.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { DataStudioComponent } from './data-studio.component'; + +/** + * Routes of conversion module. + */ +const routes: Routes = [ + { + path: '', + component: DataStudioComponent, + }, +]; + +/** + * DataStudio routing module. + */ +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class DataStudioRoutingModule {} diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.html b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.html new file mode 100644 index 0000000000..ea78922d87 --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.html @@ -0,0 +1,27 @@ +

{{ 'common.dataStudio.few' | translate }}

+ + + {{ + 'common.dataStudio.dataGeneration.few' | translate + }} +
+

+ {{ 'models.form.select' | translate }}: +

+
+
+ +
+
+
+ +
+
diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.scss b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.spec.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.spec.ts new file mode 100644 index 0000000000..b249dc09af --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataStudioComponent } from './data-studio.component'; + +describe('DataStudioComponent', () => { + let component: DataStudioComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DataStudioComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DataStudioComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.ts new file mode 100644 index 0000000000..5f9d20e75a --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.component.ts @@ -0,0 +1,104 @@ +import { Component, OnInit } from '@angular/core'; +import { + UnsubscribeComponent, + Form, + FormsQueryResponse, +} from '@oort-front/shared'; +import { + animate, + state, + style, + transition, + trigger, +} from '@angular/animations'; +import { Apollo, QueryRef } from 'apollo-angular'; +import { GET_FORM_NAMES } from './graphql/queries'; +import { FormBuilder } from '@angular/forms'; + +/** Items per page */ +const ITEMS_PER_PAGE = 10; + +/** + * Data Studio component + */ +@Component({ + selector: 'app-data-studio', + templateUrl: './data-studio.component.html', + styleUrls: ['./data-studio.component.scss'], + animations: [ + trigger('detailExpand', [ + state('collapsed', style({ height: '0px', minHeight: '0' })), + state('expanded', style({ height: '*' })), + transition( + 'expanded <=> collapsed', + animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)') + ), + ]), + ], +}) +/** Data studio component class */ +export class DataStudioComponent + extends UnsubscribeComponent + implements OnInit +{ + /** Single form */ + public formId = ''; + + /** Form handling */ + public formsQuery!: QueryRef; + public cachedForms: Form[] = []; + public selectedForm = this.fb.group({ + form: [''], + }); + + /** + * Data studio component constructor + * + * @param apollo Apollo client service + */ + constructor(private apollo: Apollo, private fb: FormBuilder) { + super(); + } + + /** OnInit Hook. */ + ngOnInit(): void { + this.formsQuery = this.apollo.watchQuery({ + query: GET_FORM_NAMES, + variables: { + first: ITEMS_PER_PAGE, + sortField: 'name', + }, + }); + } + + /** + * Update query based on text search. + * + * @param search Search text from the graphql select + */ + onSearchChange(search: string): void { + const variables = this.formsQuery.variables; + this.formsQuery.refetch({ + ...variables, + filter: { + logic: 'and', + filters: [ + { + field: 'name', + operator: 'contains', + value: search, + }, + ], + }, + }); + } + + /** + * Form selection change handler + * + * @param event + */ + onSelectionChange(event: any): void { + this.formId = event; + } +} diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.module.ts b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.module.ts new file mode 100644 index 0000000000..93284b05a3 --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/data-studio.module.ts @@ -0,0 +1,64 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DataStudioComponent } from '../data-studio/data-studio.component'; +import { + ButtonModule, + CheckboxModule, + DateModule as UiDateModule, + FormWrapperModule, + IconModule, + PaginatorModule, + SelectMenuModule, + SelectOptionModule, + SpinnerModule, + TableModule, + TabsModule, + TooltipModule, + ExpansionPanelModule, + GraphQLSelectModule, + RadioModule, + ToggleModule, +} from '@oort-front/ui'; +import { ListFilterComponent, SkeletonTableModule } from '@oort-front/shared'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; +import { DataStudioRoutingModule } from './data-studio-routing.module'; +import { DataGenerationFieldsComponent } from './data-generation-fields/data-generation-fields.component'; +import { SurveyModule } from 'survey-angular-ui'; +import { DateModule } from './../../../../../../../libs/shared/src/lib/pipes/date/date.module'; +import { InputsModule } from '@progress/kendo-angular-inputs'; +import { DateInputsModule } from '@progress/kendo-angular-dateinputs'; +@NgModule({ + declarations: [DataStudioComponent, DataGenerationFieldsComponent], + imports: [ + CommonModule, + TooltipModule, + PaginatorModule, + TranslateModule, + UiDateModule, + SkeletonTableModule, + FormsModule, + ReactiveFormsModule, + SpinnerModule, + FormWrapperModule, + IconModule, + SelectMenuModule, + ButtonModule, + TableModule, + DateModule, + ListFilterComponent, + DataStudioRoutingModule, + SelectOptionModule, + TabsModule, + CheckboxModule, + ExpansionPanelModule, + GraphQLSelectModule, + RadioModule, + ToggleModule, + SurveyModule, + DateModule, + InputsModule, + DateInputsModule, + ], +}) +export class DataStudioModule {} diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/graphql/mutations.ts b/apps/back-office/src/app/dashboard/pages/data-studio/graphql/mutations.ts new file mode 100644 index 0000000000..32564e27ef --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/graphql/mutations.ts @@ -0,0 +1,26 @@ +import { gql } from 'apollo-angular'; + +/** Graphql query for converting records of a resource */ +export const CONVERT_RESOURCE_RECORDS = gql` + mutation convertRecords( + $id: ID! + $initialType: String! + $newType: String! + $field: String! + $popArray: String + $failedAction: String! + ) { + convertRecords( + id: $id + initialType: $initialType + newType: $newType + field: $field + popArray: $popArray + failedAction: $failedAction + ) { + id + createdAt + modifiedAt + } + } +`; diff --git a/apps/back-office/src/app/dashboard/pages/data-studio/graphql/queries.ts b/apps/back-office/src/app/dashboard/pages/data-studio/graphql/queries.ts new file mode 100644 index 0000000000..97d329fd4f --- /dev/null +++ b/apps/back-office/src/app/dashboard/pages/data-studio/graphql/queries.ts @@ -0,0 +1,112 @@ +import { gql } from 'apollo-angular'; + +/** GraphQL resource fields for role summary */ +export const RESOURCE_FIELDS = gql` + fragment ResourceFields on Resource { + id + name + metadata { + name + type + editor + filter + multiSelect + options + fields { + name + type + editor + filter + multiSelect + options + } + usedIn + } + fields + } +`; + +/** GraphQL short resource fields for role summary */ +export const SHORT_RESOURCE_FIELDS = gql` + fragment ShortResourceFields on Resource { + id + name + fields + } +`; + +/** Graphql query for getting resources with a filter and more data */ +export const GET_RESOURCES = gql` + query GetResources( + $first: Int + $afterCursor: ID + $filter: JSON + $sortField: String + $sortOrder: String + ) { + resources( + first: $first + afterCursor: $afterCursor + filter: $filter + sortField: $sortField + sortOrder: $sortOrder + ) { + edges { + node { + ...ShortResourceFields + } + cursor + } + totalCount + pageInfo { + hasNextPage + endCursor + } + } + } + ${SHORT_RESOURCE_FIELDS} +`; + +/** GraphQL query to get a single resource */ +export const GET_RESOURCE = gql` + query GetResources($id: ID!) { + resource(id: $id) { + forms { + id + name + } + ...ResourceFields + } + } + ${RESOURCE_FIELDS} +`; + +/** Graphql query for getting form names */ +export const GET_FORM_NAMES = gql` + query GetFormNames( + $first: Int + $afterCursor: ID + $sortField: String + $filter: JSON + ) { + forms( + first: $first + afterCursor: $afterCursor + sortField: $sortField + filter: $filter + ) { + edges { + node { + id + name + } + cursor + } + totalCount + pageInfo { + hasNextPage + endCursor + } + } + } +`; diff --git a/libs/shared/src/i18n/en.json b/libs/shared/src/i18n/en.json index 0e0cab75d0..111cbea0e9 100644 --- a/libs/shared/src/i18n/en.json +++ b/libs/shared/src/i18n/en.json @@ -152,6 +152,59 @@ "none": "No dashboard", "one": "Dashboard" }, + "dataStudio": { + "dataGeneration": { + "few": "Data Generation", + "fieldType": "Field type: ", + "fieldsTooltip": "List of the fields from the form to generate data for. Select the fields you want to include by marking the checkboxes. You can also see the field type and the generation source and choose between multiple generation sources in some cases.", + "generate": "Generate", + "generationNotPossible": "Values for this field type cannot be generated.", + "generationSource": "Generation source: ", + "map": { + "boolean": "Randomly true or false", + "checkbox": "Random choices of question choices", + "color": "Random color", + "date": "Random date", + "datetimelocal": "Random date and time", + "dropdown": "Random choice of question choices", + "email": "Random email", + "expression": "Solved expression value", + "geospatial": "Random location", + "matrix": "Random selection for each row", + "matrixdropdown": "Random generation for each column type for each row", + "matrixdynamic": "Random generation for each column type for each row", + "month": "Random month", + "multipletext": "Random line for each text field", + "numeric": "Random number", + "owner": "Random roles from the preselected application", + "paneldynamic": "Generate 1-5 entries for each panel", + "password": "Random password", + "radiogroup": "Random choice of question choices", + "range": "Random range (0 to 100)", + "resource": "Random record from the resource", + "resources": "Random records from the resource", + "sentence": "Random sentence", + "tagbox": "Random choices of question choices", + "tel": "Random phone number", + "text": "Random text", + "time": "Random time", + "url": "Random URL", + "users": "Random users", + "week": "Random week" + }, + "maxDate": "Max date (optional)", + "maxNumber": "Max number (optional)", + "maxTime": "Max time (optional)", + "minDate": "Min date (optional)", + "minNumber": "Min number (optional)", + "minTime": "Min time (optional)", + "recordsNumber": "Number of records (max: 50)", + "selectAll": "Select All", + "setDefault": "Set default", + "setDefaultTooltip": "Leave this option off to generate random data for the field. If you want to set a default value for the field, turn this option on and fill in the survey." + }, + "few": "Data Studio" + }, "delete": "Delete", "deleteObject": "Delete {{name}}", "deleted": "Deleted", @@ -229,6 +282,7 @@ "accessNotProvided": "No access provided to this {{type}}. {{error}}", "alreadyExists": "The {{type}} {{value}} already exists on this application.", "copiedToClipboard": "Dashboard link copied!", + "dataGenerated": "new record(s) successfully generated.", "dataNotRecovered": "Failure on data recovery", "dataRecovered": "The data has been recovered", "email": { @@ -525,6 +579,7 @@ }, "customStyling": "Custom Styling", "dashboard": { + "clearFields": "Clear fields", "editFilter": "Edit filter", "empty": "No content available. Wait for your administrator to build some.", "filter": { diff --git a/libs/shared/src/i18n/fr.json b/libs/shared/src/i18n/fr.json index e1187afe47..3979a59e7a 100644 --- a/libs/shared/src/i18n/fr.json +++ b/libs/shared/src/i18n/fr.json @@ -152,6 +152,59 @@ "none": "Aucun tableau de bord", "one": "Tableau de bord" }, + "dataStudio": { + "dataGeneration": { + "few": "Génération de données", + "fieldType": "Type de champ : ", + "fieldsTooltip": "Liste des champs du formulaire pour lesquels des données doivent être générées. Sélectionnez les champs que vous souhaitez inclure en cochant les cases correspondantes. Vous pouvez également voir le type de champ et la source de génération et choisir entre plusieurs sources de génération dans certains cas.", + "generate": "Générer", + "generationNotPossible": "Les valeurs de ce type de champ ne peuvent pas être générées.", + "generationSource": "Source de génération : ", + "map": { + "boolean": "Vrai ou faux au hasard", + "checkbox": "Choix aléatoire des questions", + "color": "Couleur aléatoire", + "date": "Date aléatoire", + "datetimelocal": "Date et heure aléatoires", + "dropdown": "Choix aléatoire des questions", + "email": "Courriel aléatoire", + "expression": "Valeur de l'expression résolue", + "geospatial": "Emplacement aléatoire", + "matrix": "Sélection aléatoire pour chaque ligne", + "matrixdropdown": "Génération aléatoire pour chaque type de colonne pour chaque ligne", + "matrixdynamic": "Génération aléatoire pour chaque type de colonne pour chaque ligne", + "month": "Mois aléatoire", + "multipletext": "Ligne aléatoire pour chaque champ de texte", + "numeric": "Nombre aléatoire", + "owner": "Rôles aléatoires à partir de l'application présélectionnée", + "paneldynamic": "Générez 1 à 5 entrées pour chaque panneau", + "password": "Mot de passe aléatoire", + "radiogroup": "Choix aléatoire des questions", + "range": "Plage aléatoire (0 à 100)", + "resource": "Enregistrement aléatoire de la ressource", + "resources": "Enregistrements aléatoires de la ressource", + "sentence": "Phrase aléatoire", + "tagbox": "Choix aléatoire des questions", + "tel": "Numéro de téléphone aléatoire", + "text": "Texte aléatoire", + "time": "Temps aléatoire", + "url": "URL aléatoire", + "users": "Utilisateurs aléatoires", + "week": "Semaine aléatoire" + }, + "maxDate": "Date maximale (facultatif)", + "maxNumber": "Nombre maximal (facultatif)", + "maxTime": "Heure maximale (facultatif)", + "minDate": "Date minimale (facultatif)", + "minNumber": "Nombre minimal (facultatif)", + "minTime": "Heure minimale (facultatif)", + "recordsNumber": "Nombre d'enregistrements (max : 50)", + "selectAll": "Sélectionner tout", + "setDefault": "Définir la valeur par défaut", + "setDefaultTooltip": "Laissez cette option désactivée pour générer des données aléatoires pour le champ. Si vous souhaitez définir une valeur par défaut pour le champ, activez cette option et remplissez le questionnaire." + }, + "few": "Data Studio" + }, "delete": "Supprimer", "deleteObject": "Supprimer {{name}}", "deleted": "Supprimé", @@ -229,6 +282,7 @@ "accessNotProvided": "Pas d'accès fourni à cet objet de type {{type}}. {{error}}", "alreadyExists": "Cet objet {{type}} {{value}} existe déjà dans cette application.", "copiedToClipboard": "Lien du tableau de bord copié !", + "dataGenerated": "le(s) nouvel(aux) enregistrement(s) a (ont) été généré(s) avec succès.", "dataNotRecovered": "Échec de la récupération des données", "dataRecovered": "Les données ont été récupérées !", "email": { @@ -525,6 +579,7 @@ }, "customStyling": "Style personnalisé", "dashboard": { + "clearFields": "Supprimer les champs", "editFilter": "Modifier le filtre", "empty": "Aucun contenu n'est disponible. Veuillez attendre que votre administrateur en crée.", "filter": { diff --git a/libs/shared/src/i18n/test.json b/libs/shared/src/i18n/test.json index f7a0dc3f79..e0617fb50f 100644 --- a/libs/shared/src/i18n/test.json +++ b/libs/shared/src/i18n/test.json @@ -152,6 +152,59 @@ "none": "******", "one": "******" }, + "dataStudio": { + "dataGeneration": { + "few": "******", + "fieldType": "******", + "fieldsTooltip": "******", + "generate": "******", + "generationNotPossible": "******", + "generationSource": "******", + "map": { + "boolean": "******", + "checkbox": "******", + "color": "******", + "date": "******", + "datetimelocal": "******", + "dropdown": "******", + "email": "******", + "expression": "******", + "geospatial": "******", + "matrix": "******", + "matrixdropdown": "******", + "matrixdynamic": "******", + "month": "******", + "multipletext": "******", + "numeric": "******", + "owner": "******", + "paneldynamic": "******", + "password": "******", + "radiogroup": "******", + "range": "******", + "resource": "******", + "resources": "******", + "sentence": "******", + "tagbox": "******", + "tel": "******", + "text": "******", + "time": "******", + "url": "******", + "users": "******", + "week": "******" + }, + "maxDate": "******", + "maxNumber": "******", + "maxTime": "******", + "minDate": "******", + "minNumber": "******", + "minTime": "******", + "recordsNumber": "******", + "selectAll": "******", + "setDefault": "******", + "setDefaultTooltip": "******" + }, + "few": "******" + }, "delete": "******", "deleteObject": "****** {{name}}", "deleted": "******", @@ -229,6 +282,7 @@ "accessNotProvided": "****** {{type}} ****** {{error}}", "alreadyExists": "****** {{type}} ****** {{value}} ******", "copiedToClipboard": "******", + "dataGenerated": "******", "dataNotRecovered": "******", "dataRecovered": "******", "email": { @@ -525,6 +579,7 @@ }, "customStyling": "******", "dashboard": { + "clearFields": "******", "editFilter": "******", "empty": "******", "filter": { diff --git a/libs/shared/src/lib/models/record.model.ts b/libs/shared/src/lib/models/record.model.ts index 2cdd9186a8..5408c73ad0 100644 --- a/libs/shared/src/lib/models/record.model.ts +++ b/libs/shared/src/lib/models/record.model.ts @@ -36,6 +36,11 @@ export interface AddRecordMutationResponse { addRecord: Record; } +/** Model for generate records graphql mutation response */ +export interface GenerateRecordsMutationResponse { + generateRecords: Record[]; +} + /** Model for edit record graphql mutation response */ export interface EditRecordMutationResponse { editRecord: Record; diff --git a/libs/ui/src/lib/expansion-panel/expansion-panel.component.ts b/libs/ui/src/lib/expansion-panel/expansion-panel.component.ts index 4a7f694426..ce2086ecfe 100644 --- a/libs/ui/src/lib/expansion-panel/expansion-panel.component.ts +++ b/libs/ui/src/lib/expansion-panel/expansion-panel.component.ts @@ -55,6 +55,8 @@ export class ExpansionPanelComponent implements AfterViewInit { @Input() index = 0; /** Event emitter for closing the panel. */ @Output() closePanel = new EventEmitter(); + /** Event emitter for opening the panel. */ + @Output() openPanel = new EventEmitter(); /** Reference to the accordion item. */ @ViewChild('accordionItem') accordionItem!: CdkAccordionItem; /** Reference to the content container. */ @@ -90,5 +92,6 @@ export class ExpansionPanelComponent implements AfterViewInit { onOpened() { this.renderer.removeClass(this.contentContainer.nativeElement, 'hidden'); this.renderer.addClass(this.contentContainer.nativeElement, 'block'); + this.openPanel.emit(true); } }