diff --git a/src/app/content/html/html-editor/html-editor.component.ts b/src/app/content/html/html-editor/html-editor.component.ts index a329847a9..3315d2b82 100644 --- a/src/app/content/html/html-editor/html-editor.component.ts +++ b/src/app/content/html/html-editor/html-editor.component.ts @@ -36,7 +36,7 @@ import { filter } from 'rxjs/operators'; import { HtmlParserService } from '../html-parser/html-parser.service'; import { SafePipe } from '../../safe.pipe'; import { FormsModule } from '@angular/forms'; -import { NgxJoditComponent } from 'ngx-jodit'; +import { JoditConfig, NgxJoditComponent } from 'ngx-jodit'; import { NgIf } from '@angular/common'; const basicButtons = [ @@ -116,7 +116,7 @@ export class HtmlEditorComponent implements OnInit, OnChanges { @Input() buttonMode: HtmlButtonMode; ButtonModeType = HtmlButtonMode; - editorConfig: object; + editorConfig: JoditConfig; displayContent = ''; @@ -190,6 +190,7 @@ export class HtmlEditorComponent implements OnInit, OnChanges { } const focusNode = editor.selection.sel.focusNode; + const savedSelection = editor.selection.save(); component.elementSelectorSubscription = component.messageService.elementSelector.subscribe( (element: MauroItem & Pathable) => { @@ -197,7 +198,7 @@ export class HtmlEditorComponent implements OnInit, OnChanges { return; } - this.createAndInsertLink(this, editor, focusNode, element); + this.createAndInsertLink(this, editor, focusNode, element, savedSelection); } ); @@ -211,7 +212,7 @@ export class HtmlEditorComponent implements OnInit, OnChanges { if (!component.allowAutocompleteSearch) { return; } - + const savedSelection = editor.selection.save(); const focusNode = editor.selection.sel.focusNode; component.dialog @@ -223,7 +224,8 @@ export class HtmlEditorComponent implements OnInit, OnChanges { component, editor, focusNode, - response.selected + response.selected, + savedSelection ); }); } @@ -232,7 +234,8 @@ export class HtmlEditorComponent implements OnInit, OnChanges { component: HtmlEditorComponent, editor: any, focusNode: any, - element: MauroItem & Pathable + element: MauroItem & Pathable, + savedSelection: any ) { const path = element.path ?? component.pathNames.createFromBreadcrumbs(element); @@ -251,8 +254,9 @@ export class HtmlEditorComponent implements OnInit, OnChanges { const html = editor.create.fromHTML( `${element.label}` ); - - editor.selection.setCursorIn(focusNode); + editor.selection.restore(savedSelection); + // editor.selection.setCursorIn(focusNode); + editor.selection.focus(); editor.selection.insertHTML(html); return { path, element }; diff --git a/src/app/mauro/profiles/default-profile-provider.service.spec.ts b/src/app/mauro/profiles/default-profile-provider.service.spec.ts index f05ed7d02..08a5b0c3a 100644 --- a/src/app/mauro/profiles/default-profile-provider.service.spec.ts +++ b/src/app/mauro/profiles/default-profile-provider.service.spec.ts @@ -302,7 +302,8 @@ describe('DefaultProfileProviderService', () => { sections: [ { name: 'Default', - fields + fields, + sections: [] } ] }; diff --git a/src/app/mauro/profiles/default-profile-provider.service.ts b/src/app/mauro/profiles/default-profile-provider.service.ts index 41cbaba91..e7335f637 100644 --- a/src/app/mauro/profiles/default-profile-provider.service.ts +++ b/src/app/mauro/profiles/default-profile-provider.service.ts @@ -387,6 +387,7 @@ export class DefaultProfileProviderService implements ProfileProviderService { sections: [ { name: defaultProfileSectionName, + sections: [], fields } ] diff --git a/src/app/modals/add-profile-modal/add-profile-modal.component.html b/src/app/modals/add-profile-modal/add-profile-modal.component.html index 400073b7a..460e406ec 100644 --- a/src/app/modals/add-profile-modal/add-profile-modal.component.html +++ b/src/app/modals/add-profile-modal/add-profile-modal.component.html @@ -52,7 +52,7 @@

Add New Profile

[disabled]="!data.selectedProfile" class="custom" > - Save Changes + Add Profile diff --git a/src/app/modals/edit-profile-modal/edit-profile-modal.component.html b/src/app/modals/edit-profile-modal/edit-profile-modal.component.html index a93fdb495..6a8fd952b 100644 --- a/src/app/modals/edit-profile-modal/edit-profile-modal.component.html +++ b/src/app/modals/edit-profile-modal/edit-profile-modal.component.html @@ -37,171 +37,14 @@

Edit Profile - {{ data.profileName }}

finalised. - - - - - - - + + diff --git a/src/app/modals/edit-profile-modal/edit-profile-modal.component.ts b/src/app/modals/edit-profile-modal/edit-profile-modal.component.ts index 3dad269d4..38df9d713 100644 --- a/src/app/modals/edit-profile-modal/edit-profile-modal.component.ts +++ b/src/app/modals/edit-profile-modal/edit-profile-modal.component.ts @@ -52,13 +52,14 @@ import { MatTooltip } from '@angular/material/tooltip'; import { AlertComponent } from '../../shared/alert/alert.component'; import { FormsModule } from '@angular/forms'; import { NgIf, NgFor, NgClass } from '@angular/common'; +import { EditProfileSectionComponent } from '@mdm/modals/edit-profile-section/edit-profile-section.component'; @Component({ selector: 'mdm-edit-profile-modal', templateUrl: './edit-profile-modal.component.html', styleUrls: ['./edit-profile-modal.component.scss'], standalone: true, - imports: [MatDialogTitle, NgIf, MatDialogContent, FormsModule, AlertComponent, NgFor, MatTooltip, ContentEditorComponent, MatInput, NgClass, ExtendedModule, MatButton, MatFormField, MatSelect, MatOption, ModelSelectorTreeComponent, MatError, MatDialogActions] + imports: [MatDialogTitle, NgIf, MatDialogContent, FormsModule, AlertComponent, NgFor, MatTooltip, ContentEditorComponent, MatInput, NgClass, ExtendedModule, MatButton, MatFormField, MatSelect, MatOption, ModelSelectorTreeComponent, MatError, MatDialogActions, EditProfileSectionComponent] }) export class EditProfileModalComponent implements OnInit { profileData: Profile; @@ -117,7 +118,7 @@ export class EditProfileModalComponent implements OnInit { ) { field.currentValue = null; } - else { + else { field.currentValue = JSON.parse(field.currentValue); } } @@ -278,7 +279,7 @@ export class EditProfileModalComponent implements OnInit { private loadDefaultCustomProfile(properties: ApiProperty[]) { this.showCanEditPropertyAlert = JSON.parse( - this.getContentProperty(properties, this.showCanEditPropertyAlertKey) + this.getContentProperty(properties, this.showCanEditPropertyAlertKey) || 'false' ); } diff --git a/src/app/modals/edit-profile-section/edit-profile-section.component.html b/src/app/modals/edit-profile-section/edit-profile-section.component.html new file mode 100644 index 000000000..47176213d --- /dev/null +++ b/src/app/modals/edit-profile-section/edit-profile-section.component.html @@ -0,0 +1,189 @@ + + + + + + + + + + + + + +
+

{{ profileSection.name }}

+

+ {{ profileSection.description }} +

+
+ {{ field.fieldName }} + * + + + + + +
+ {{ field.currentValue }} + +
+ + +
+ + +
+
+ + + + {{ value }} + + + +
+
+ + +
+ + {{ getValidationError(field.metadataPropertyName).message }} + +
+ \ No newline at end of file diff --git a/src/app/modals/edit-profile-section/edit-profile-section.component.scss b/src/app/modals/edit-profile-section/edit-profile-section.component.scss new file mode 100644 index 000000000..bab5954f2 --- /dev/null +++ b/src/app/modals/edit-profile-section/edit-profile-section.component.scss @@ -0,0 +1,23 @@ +/* +Copyright 2020-2025 University of Oxford and NHS England + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +*/ +.containerStyle { + border: solid; + border-width: 1px; + padding: 4px; + margin-top: 5px; +} \ No newline at end of file diff --git a/src/app/modals/edit-profile-section/edit-profile-section.component.ts b/src/app/modals/edit-profile-section/edit-profile-section.component.ts new file mode 100644 index 000000000..da2e223d3 --- /dev/null +++ b/src/app/modals/edit-profile-section/edit-profile-section.component.ts @@ -0,0 +1,173 @@ +/* +Copyright 2020-2025 University of Oxford and NHS England + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +*/ + +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { + ApiProperty, + ApiPropertyIndexResponse, Pathable, + Profile, + ProfileField, ProfileSection, + ProfileValidationError, + ProfileValidationErrorList +} from '@maurodatamapper/mdm-resources'; +import { ModalDialogStatus } from '@mdm/constants/modal-dialog-status'; +import { MdmResourcesService } from '@mdm/modules/resources'; +import { ElementSelectorComponent } from '@mdm/utility/element-selector.component'; +import { MessageHandlerService } from '@mdm/services'; +import { MarkdownParserService } from '@mdm/content/markdown/markdown-parser/markdown-parser.service'; +import { EMPTY, Observable, of } from 'rxjs'; +import { catchError, finalize, map, switchMap } from 'rxjs/operators'; +import { + EditProfileModalConfiguration, + EditProfileModalResult +} from './edit-profile-section.model'; +import { EditingService } from '@mdm/services/editing.service'; +import { MauroItem } from '@mdm/mauro/mauro-item.types'; +import { ModelSelectorTreeComponent } from '@mdm/model-selector-tree/model-selector-tree.component'; +import { MatOption } from '@angular/material/core'; +import { MatSelect } from '@angular/material/select'; +import { MatFormField, MatError } from '@angular/material/form-field'; +import { MatButton } from '@angular/material/button'; +import { ExtendedModule } from '@angular/flex-layout/extended'; +import { MatInput } from '@angular/material/input'; +import { ContentEditorComponent } from '@mdm/content/content-editor/content-editor.component'; +import { MatTooltip } from '@angular/material/tooltip'; +import { FormsModule } from '@angular/forms'; +import { NgIf, NgFor, NgClass } from '@angular/common'; + +@Component({ + selector: '[mdm-edit-profile-section]', + templateUrl: './edit-profile-section.component.html', + styleUrls: ['./edit-profile-section.component.scss'], + standalone: true, + imports: [NgIf, FormsModule, NgFor, MatTooltip, ContentEditorComponent, MatInput, NgClass, ExtendedModule, MatButton, MatFormField, MatSelect, MatOption, ModelSelectorTreeComponent, MatError] +}) +export class EditProfileSectionComponent implements OnInit { + @Input() profileSection: ProfileSection; + + description?: string; + okBtnText: string; + showCanEditPropertyAlert = true; + validationErrors: ProfileValidationErrorList = { + total: 0, + fieldTotal: 0, + errors: [] + }; + + isValidated = false; + + formOptionsMap = { + integer: 'number', + string: 'text', + boolean: 'checkbox', + int: 'number', + date: 'date', + time: 'time', + datetime: 'datetime', + decimal: 'number' + }; + + private readonly showCanEditPropertyAlertKey + = 'ui.show_can_edit_property_alert'; + + constructor( + public dialogRef: MatDialogRef< + EditProfileSectionComponent, + EditProfileModalResult + >, + @Inject(MAT_DIALOG_DATA) public data: EditProfileModalConfiguration, + private markdownParser: MarkdownParserService, + protected resources: MdmResourcesService, + private dialog: MatDialog, + private messageHandler: MessageHandlerService, + protected editing: EditingService + ) { + data.profile.sections.forEach((section) => { + section.fields.forEach((field) => { + if (data.isNew && field.defaultValue) { + field.currentValue = field.defaultValue; + } + if (field.dataType === 'boolean') { + if (field.currentValue !== 'true') { + field.currentValue = 'false'; + } + } + if (field.dataType === 'folder') { + if ( + field.currentValue === '[]' + || field.currentValue === '""' + || field.currentValue === '' + ) { + field.currentValue = null; + } + else { + field.currentValue = JSON.parse(field.currentValue); + } + } + + this.attachReadOnlyPropertyToField(field); + }); + }); + + this.description = data.description; + this.okBtnText = data.okBtn ?? 'Save'; + } + + ngOnInit(): void { + } + + save() { + } + + getValidationError( + metadataPropertyName: string + ): ProfileValidationError | undefined { + if (this.validationErrors.fieldTotal === 0) { + return undefined; + } + + return this.validationErrors.errors.find( + e => e.metadataPropertyName === metadataPropertyName + ); + } + + sortBy(items: []) { + return items.sort((a, b) => (a > b ? 1 : a === b ? 0 : -1)); + } + + showAddElementToMarkdown(field: ProfileField) { + const dg = this.dialog.open(ElementSelectorComponent, { + data: { validTypesToSelect: ['DataModel'], notAllowedToSelectIds: [] }, + panelClass: 'element-selector-modal' + }); + + dg.afterClosed().subscribe((dgData) => { + const link = this.markdownParser.createMarkdownLink(dgData as MauroItem & Pathable); + field.currentValue = link; + }); + } + + private attachReadOnlyPropertyToField(field: ProfileField) { + field.readOnly + = field.uneditable + || (this.data.finalised && !field.editableAfterFinalisation); + } + +} diff --git a/src/app/modals/edit-profile-section/edit-profile-section.model.ts b/src/app/modals/edit-profile-section/edit-profile-section.model.ts new file mode 100644 index 000000000..21f02ebb8 --- /dev/null +++ b/src/app/modals/edit-profile-section/edit-profile-section.model.ts @@ -0,0 +1,35 @@ +/* +Copyright 2020-2025 University of Oxford and NHS England + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +*/ + +import { CatalogueItem, Profile } from '@maurodatamapper/mdm-resources'; +import { ModalDialogStatus } from '@mdm/constants/modal-dialog-status'; + +export interface EditProfileModalConfiguration { + profile: Profile + profileName: string + catalogueItem: CatalogueItem + isNew: boolean + description?: string + okBtn?: string + finalised?: boolean +} + +export interface EditProfileModalResult { + status: ModalDialogStatus + profile?: Profile +} diff --git a/src/app/services/element-selector-dialogue.service.ts b/src/app/services/element-selector-dialogue.service.ts index 195f4cc52..f2ed9b209 100644 --- a/src/app/services/element-selector-dialogue.service.ts +++ b/src/app/services/element-selector-dialogue.service.ts @@ -27,7 +27,7 @@ export class ElementSelectorDialogueService { open(validTypesToSelect, notAllowedToSelectIds) { if (!validTypesToSelect || (validTypesToSelect && validTypesToSelect.length === 0)) { - validTypesToSelect = ['Folder','CodeSet', 'DataModel', 'DataClass', 'DataType', 'DataElement', 'Term']; + validTypesToSelect = ['Folder', 'CodeSet', 'DataModel', 'DataClass', 'DataType', 'DataElement', 'Term']; } const dg = this.dialog.open(ElementSelectorComponent, { diff --git a/src/app/shared/profile-details/profile-details.component.html b/src/app/shared/profile-details/profile-details.component.html index f602796bf..cd00e07f7 100644 --- a/src/app/shared/profile-details/profile-details.component.html +++ b/src/app/shared/profile-details/profile-details.component.html @@ -16,86 +16,9 @@ SPDX-License-Identifier: Apache-2.0 --> - - - - - - - + + diff --git a/src/app/shared/profile-details/profile-details.component.ts b/src/app/shared/profile-details/profile-details.component.ts index ba2bc2cf9..3d230f3eb 100644 --- a/src/app/shared/profile-details/profile-details.component.ts +++ b/src/app/shared/profile-details/profile-details.component.ts @@ -23,13 +23,14 @@ import { FormsModule } from '@angular/forms'; import { ContentEditorComponent } from '../../content/content-editor/content-editor.component'; import { MatTooltip } from '@angular/material/tooltip'; import { NgIf, NgFor } from '@angular/common'; +import { ProfileSectionComponent } from '@mdm/shared/profile-section/profile-section.component'; @Component({ selector: 'mdm-profile-details', templateUrl: './profile-details.component.html', styleUrls: ['./profile-details.component.scss'], standalone: true, - imports: [NgIf, NgFor, MatTooltip, ContentEditorComponent, FormsModule, MoreDescriptionComponent] + imports: [NgIf, NgFor, MatTooltip, ContentEditorComponent, FormsModule, MoreDescriptionComponent, ProfileSectionComponent] }) export class ProfileDetailsComponent { @Input() currentProfileDetails: Profile; diff --git a/src/app/shared/profile-section/profile-section.component.html b/src/app/shared/profile-section/profile-section.component.html new file mode 100644 index 000000000..165d44174 --- /dev/null +++ b/src/app/shared/profile-section/profile-section.component.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/shared/profile-section/profile-section.component.scss b/src/app/shared/profile-section/profile-section.component.scss new file mode 100644 index 000000000..adb47e3ef --- /dev/null +++ b/src/app/shared/profile-section/profile-section.component.scss @@ -0,0 +1,17 @@ +/* +Copyright 2020-2025 University of Oxford and NHS England + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +*/ diff --git a/src/app/shared/profile-section/profile-section.component.ts b/src/app/shared/profile-section/profile-section.component.ts new file mode 100644 index 000000000..04f0b71e2 --- /dev/null +++ b/src/app/shared/profile-section/profile-section.component.ts @@ -0,0 +1,48 @@ +/* +Copyright 2020-2025 University of Oxford and NHS England + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +SPDX-License-Identifier: Apache-2.0 +*/ + +import { Component, Input } from '@angular/core'; +import { ProfileSection } from '@maurodatamapper/mdm-resources'; +import { MoreDescriptionComponent } from '../more-description/more-description.component'; +import { FormsModule } from '@angular/forms'; +import { ContentEditorComponent } from '@mdm/content/content-editor/content-editor.component'; +import { MatTooltip } from '@angular/material/tooltip'; +import { NgIf, NgFor } from '@angular/common'; +import { EditProfileSectionComponent } from '@mdm/modals/edit-profile-section/edit-profile-section.component'; + +@Component({ + selector: '[mdm-profile-section]', + templateUrl: './profile-section.component.html', + styleUrls: ['./profile-section.component.scss'], + standalone: true, + imports: [NgIf, NgFor, MatTooltip, ContentEditorComponent, FormsModule, MoreDescriptionComponent, EditProfileSectionComponent] +}) +export class ProfileSectionComponent { + @Input() profileSection: ProfileSection; + + readonly formOptionsMap = { + integer: 'number', + string: 'text', + boolean: 'checkbox', + int: 'number', + date: 'date', + time: 'time', + datetime: 'datetime', + decimal: 'number' + }; +} diff --git a/src/app/utility/element-selector.component.html b/src/app/utility/element-selector.component.html index a789b1e52..9d7231da8 100644 --- a/src/app/utility/element-selector.component.html +++ b/src/app/utility/element-selector.component.html @@ -22,7 +22,7 @@ What type of element are you looking for?
- -
+ +
Please select a Folder:
diff --git a/src/app/utility/element-selector.component.spec.ts b/src/app/utility/element-selector.component.spec.ts index f06f892be..e0ca475f3 100644 --- a/src/app/utility/element-selector.component.spec.ts +++ b/src/app/utility/element-selector.component.spec.ts @@ -30,7 +30,6 @@ import { ModelSelectorTreeComponent } from '@mdm/model-selector-tree/model-selec import { ModelPathComponent } from './model-path/model-path.component'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MdmResourcesService } from '@mdm/modules/resources'; -import { UIRouterModule } from '@uirouter/angular'; import { ToastrModule } from 'ngx-toastr'; describe('ElementSelectorComponent', () => { diff --git a/src/app/utility/element-selector.component.ts b/src/app/utility/element-selector.component.ts index dd6e1feb7..0495fabf6 100644 --- a/src/app/utility/element-selector.component.ts +++ b/src/app/utility/element-selector.component.ts @@ -45,7 +45,6 @@ import { MatDialogRef, MAT_DIALOG_DATA, MatDialogContent, MatDialogActions } fro import { GridService } from '@mdm/services/grid.service'; import { ContainerDomainType, - FolderIndexResponse, MdmTreeItemListResponse, Terminology, SearchQueryParameters @@ -223,22 +222,22 @@ export class ElementSelectorComponent implements OnInit { } loadAllFolders = () => { - this.reloading = true; - this.resourceService.tree - .list(ContainerDomainType.Folders, { domainType: 'folders' }) - .subscribe( - (data) => { - this.rootNode = { - children: data.body, - isRoot: true - }; - this.reloading = false; - }, - () => { - this.reloading = false; - } - ); -}; + this.reloading = true; + this.resourceService.tree + .list(ContainerDomainType.Folders, { domainType: 'folders' }) + .subscribe( + (data) => { + this.rootNode = { + children: data.body, + isRoot: true + }; + this.reloading = false; + }, + () => { + this.reloading = false; + } + ); + }; onTerminologySelect = (terminology: any) => { this.dataSource = new MatTableDataSource(null); diff --git a/src/style/components/_table.scss b/src/style/components/_table.scss index 017763749..fdcdf1457 100644 --- a/src/style/components/_table.scss +++ b/src/style/components/_table.scss @@ -20,6 +20,10 @@ table.mat-mdc-table .mat-mdc-header-cell { font-weight: 600; } +table tbody { + width: 100%; +} + .table-striped tbody tr:nth-of-type(even) { background-color: #fcfcfd; } diff --git a/src/style/layout/_tree.scss b/src/style/layout/_tree.scss index 398d67953..b9523a21d 100644 --- a/src/style/layout/_tree.scss +++ b/src/style/layout/_tree.scss @@ -215,8 +215,8 @@ tr th.mat-mdc-header-cell { } .detailsRowHeader { - max-width: 180px; - width: 180px; + max-width: 20em; + width: 20em; @include respond-to($mobile-xs) { max-width: 140px; width: 140px;