From 1adf825c3ffad32a5a36fbde2999a7199ee4d9a1 Mon Sep 17 00:00:00 2001
From: andrej romanov <50377758+auumgn@users.noreply.github.com>
Date: Tue, 23 Apr 2024 23:41:59 +0300
Subject: [PATCH 1/3] add member info edit component

---
 .../app/entities/member/member.service.ts     |   4 +-
 ui/angular.json                               |   5 +-
 ui/package-lock.json                          | 697 ++++++++++++++++--
 ui/package.json                               |   3 +
 .../app/affiliation/affiliations.component.ts |   1 +
 ui/src/app/home/home.component.scss           |   1 +
 ui/src/app/home/home.module.ts                |   7 +-
 ui/src/app/home/home.route.ts                 |  19 +
 .../member-info-edit.component.html           | 440 +++++++++++
 .../member-info-edit.component.scss           | 130 ++++
 .../member-info-edit.component.spec.ts        |  21 +
 .../member-info/member-info-edit.component.ts | 227 ++++++
 .../home/member-info/member-info.component.ts |   1 -
 ui/src/app/member/service/member.service.ts   |  39 +-
 ui/src/index.html                             |   2 -
 15 files changed, 1538 insertions(+), 59 deletions(-)
 create mode 100644 ui/src/app/home/member-info/member-info-edit.component.html
 create mode 100644 ui/src/app/home/member-info/member-info-edit.component.scss
 create mode 100644 ui/src/app/home/member-info/member-info-edit.component.spec.ts
 create mode 100644 ui/src/app/home/member-info/member-info-edit.component.ts

diff --git a/gateway/src/main/webapp/app/entities/member/member.service.ts b/gateway/src/main/webapp/app/entities/member/member.service.ts
index 8ba2d4ef3..3f8632cea 100644
--- a/gateway/src/main/webapp/app/entities/member/member.service.ts
+++ b/gateway/src/main/webapp/app/entities/member/member.service.ts
@@ -165,8 +165,8 @@ export class MSMemberService {
     return this.http.delete<any>(`${this.resourceUrl}/members/${id}`, { observe: 'response' });
   }
 
-  updateMemberDetails(memberDetails: ISFMemberUpdate, salesforceId: string): Observable<HttpResponse<any>> {
-    return this.http.put(`${this.resourceUrl}/members/${salesforceId}/member-details`, memberDetails, { observe: 'response' });
+  updateMemberDetails(memberDetails: ISFMemberUpdate, salesforceId: string): Observable<ISFMemberUpdate> {
+    return this.http.put(`${this.resourceUrl}/members/${salesforceId}/member-details`, memberDetails);
   }
 
   getConsortiaLeadName(consortiaLeadId: string): Observable<EntityResponseType> {
diff --git a/ui/angular.json b/ui/angular.json
index 07a8d92ca..281a30a19 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -87,7 +87,10 @@
               "src/content/images",
               "src/content/css"
             ],
-            "styles": ["src/content/scss/global.scss"],
+            "styles": [
+              "src/content/scss/global.scss",
+              "node_modules/quill/dist/quill.snow.css"
+            ],
             "scripts": []
           },
           "configurations": {
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 744e83d18..01cf3ec80 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -26,9 +26,11 @@
         "jsrsasign-util": "^1.0.5",
         "moment": "^2.29.4",
         "ngx-clipboard": "^16.0.0",
+        "ngx-quill": "^23.0.2",
         "ngx-webstorage": "^12.0.0",
         "rxjs": "~7.5.0",
         "tslib": "^2.3.0",
+        "uninstall": "^0.0.0",
         "zone.js": "~0.13.3"
       },
       "devDependencies": {
@@ -42,6 +44,7 @@
         "@angular/compiler-cli": "^16.2.9",
         "@types/jasmine": "~4.0.0",
         "@types/jsrsasign": "^10.5.13",
+        "@types/quill": "^1.3.10",
         "@typescript-eslint/eslint-plugin": "5.62.0",
         "@typescript-eslint/parser": "5.62.0",
         "eslint": "^8.49.0",
@@ -4891,6 +4894,15 @@
       "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
       "dev": true
     },
+    "node_modules/@types/quill": {
+      "version": "1.3.10",
+      "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
+      "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
+      "dev": true,
+      "dependencies": {
+        "parchment": "^1.1.2"
+      }
+    },
     "node_modules/@types/range-parser": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
@@ -6245,13 +6257,18 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
-      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
-      "dev": true,
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.0.2"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -7016,6 +7033,26 @@
       "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
       "dev": true
     },
+    "node_modules/deep-equal": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
+      "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
+      "peer": true,
+      "dependencies": {
+        "is-arguments": "^1.1.1",
+        "is-date-object": "^1.0.5",
+        "is-regex": "^1.1.4",
+        "object-is": "^1.1.5",
+        "object-keys": "^1.1.1",
+        "regexp.prototype.flags": "^1.5.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -7046,6 +7083,22 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/define-lazy-prop": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@@ -7055,6 +7108,23 @@
         "node": ">=8"
       }
     },
+    "node_modules/define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "peer": true,
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -7452,6 +7522,25 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "node_modules/es-define-property": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+      "dependencies": {
+        "get-intrinsic": "^1.2.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es-module-lexer": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
@@ -8181,8 +8270,7 @@
     "node_modules/extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-      "dev": true
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
     },
     "node_modules/external-editor": {
       "version": "3.1.0",
@@ -8204,6 +8292,12 @@
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
+    "node_modules/fast-diff": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
+      "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
+      "peer": true
+    },
     "node_modules/fast-glob": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -8574,10 +8668,21 @@
       }
     },
     "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "peer": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/gauge": {
       "version": "4.0.4",
@@ -8615,14 +8720,18 @@
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
-      "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
-      "dev": true,
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.3"
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "hasown": "^2.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -8715,6 +8824,17 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "dependencies": {
+        "get-intrinsic": "^1.1.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/graceful-fs": {
       "version": "4.2.10",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@@ -8765,11 +8885,47 @@
         "node": ">=4"
       }
     },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/has-symbols": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
       "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
-      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "peer": true,
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
       "engines": {
         "node": ">= 0.4"
       },
@@ -8783,6 +8939,17 @@
       "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
       "dev": true
     },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/hdr-histogram-js": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz",
@@ -9305,6 +9472,22 @@
         "node": ">= 10"
       }
     },
+    "node_modules/is-arguments": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+      "peer": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -9334,6 +9517,21 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-date-object": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+      "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+      "peer": true,
+      "dependencies": {
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-docker": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -9438,6 +9636,22 @@
       "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
       "dev": true
     },
+    "node_modules/is-regex": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+      "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+      "peer": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/is-stream": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -11187,6 +11401,22 @@
         "@angular/core": ">=13.0.0"
       }
     },
+    "node_modules/ngx-quill": {
+      "version": "23.0.2",
+      "resolved": "https://registry.npmjs.org/ngx-quill/-/ngx-quill-23.0.2.tgz",
+      "integrity": "sha512-6NoO/J7JWMdUdFRHwLVzyYWvihilrl9zEYUp8rRBm0bNdgMPTBUmoHnMbto8ln/eNu+APOfVq+DboTx8vO1PeA==",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@angular/core": "^16.0.0",
+        "quill": "^1.3.7",
+        "rxjs": "^7.0.0"
+      }
+    },
     "node_modules/ngx-webstorage": {
       "version": "12.0.0",
       "resolved": "https://registry.npmjs.org/ngx-webstorage/-/ngx-webstorage-12.0.0.tgz",
@@ -11853,6 +12083,31 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/object-is": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+      "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+      "peer": true,
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "peer": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/object-path": {
       "version": "0.11.8",
       "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
@@ -12160,6 +12415,11 @@
       "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
       "dev": true
     },
+    "node_modules/parchment": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
+      "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -12759,6 +13019,49 @@
         }
       ]
     },
+    "node_modules/quill": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
+      "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
+      "peer": true,
+      "dependencies": {
+        "clone": "^2.1.1",
+        "deep-equal": "^1.0.1",
+        "eventemitter3": "^2.0.3",
+        "extend": "^3.0.2",
+        "parchment": "^1.1.4",
+        "quill-delta": "^3.6.2"
+      }
+    },
+    "node_modules/quill-delta": {
+      "version": "3.6.3",
+      "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
+      "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
+      "peer": true,
+      "dependencies": {
+        "deep-equal": "^1.0.1",
+        "extend": "^3.0.2",
+        "fast-diff": "1.1.2"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/quill/node_modules/clone": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+      "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+      "peer": true,
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/quill/node_modules/eventemitter3": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
+      "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
+      "peer": true
+    },
     "node_modules/randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -12907,6 +13210,24 @@
       "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==",
       "dev": true
     },
+    "node_modules/regexp.prototype.flags": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+      "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
+      "peer": true,
+      "dependencies": {
+        "call-bind": "^1.0.6",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "set-function-name": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/regexpu-core": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz",
@@ -13512,6 +13833,37 @@
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
       "dev": true
     },
+    "node_modules/set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/set-function-name": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+      "peer": true,
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -14038,9 +14390,9 @@
       }
     },
     "node_modules/tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "dev": true,
       "dependencies": {
         "chownr": "^2.0.0",
@@ -14526,6 +14878,11 @@
         "node": ">=4"
       }
     },
+    "node_modules/uninstall": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/uninstall/-/uninstall-0.0.0.tgz",
+      "integrity": "sha512-pjP/0+A4gsbDVa8XH/S2GZdT9NPJW8NFMy3GI7HnsWG+NAmFSSj3QidNosXBI9cPtxxNExEDdhKFO6sli8K3mA=="
+    },
     "node_modules/unique-filename": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
@@ -18904,6 +19261,15 @@
       "integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
       "dev": true
     },
+    "@types/quill": {
+      "version": "1.3.10",
+      "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
+      "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
+      "dev": true,
+      "requires": {
+        "parchment": "^1.1.2"
+      }
+    },
     "@types/range-parser": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
@@ -19921,13 +20287,15 @@
       }
     },
     "call-bind": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
-      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
-      "dev": true,
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
       "requires": {
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.0.2"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.1"
       }
     },
     "callsites": {
@@ -20500,6 +20868,20 @@
       "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
       "dev": true
     },
+    "deep-equal": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
+      "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
+      "peer": true,
+      "requires": {
+        "is-arguments": "^1.1.1",
+        "is-date-object": "^1.0.5",
+        "is-regex": "^1.1.4",
+        "object-is": "^1.1.5",
+        "object-keys": "^1.1.1",
+        "regexp.prototype.flags": "^1.5.1"
+      }
+    },
     "deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -20524,12 +20906,33 @@
         "clone": "^1.0.2"
       }
     },
+    "define-data-property": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+      "requires": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      }
+    },
     "define-lazy-prop": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
       "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
       "dev": true
     },
+    "define-properties": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+      "peer": true,
+      "requires": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      }
+    },
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -20843,6 +21246,19 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "es-define-property": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+      "requires": {
+        "get-intrinsic": "^1.2.4"
+      }
+    },
+    "es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
+    },
     "es-module-lexer": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
@@ -21384,8 +21800,7 @@
     "extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
-      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
-      "dev": true
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
     },
     "external-editor": {
       "version": "3.1.0",
@@ -21404,6 +21819,12 @@
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
+    "fast-diff": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
+      "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
+      "peer": true
+    },
     "fast-glob": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -21682,10 +22103,15 @@
       "optional": true
     },
     "function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+    },
+    "functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "peer": true
     },
     "gauge": {
       "version": "4.0.4",
@@ -21714,14 +22140,15 @@
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
     },
     "get-intrinsic": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
-      "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
-      "dev": true,
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
       "requires": {
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.3"
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "has-proto": "^1.0.1",
+        "has-symbols": "^1.0.3",
+        "hasown": "^2.0.0"
       }
     },
     "get-package-type": {
@@ -21781,6 +22208,14 @@
         "slash": "^4.0.0"
       }
     },
+    "gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "requires": {
+        "get-intrinsic": "^1.1.3"
+      }
+    },
     "graceful-fs": {
       "version": "4.2.10",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@@ -21822,11 +22257,32 @@
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
     },
+    "has-property-descriptors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+      "requires": {
+        "es-define-property": "^1.0.0"
+      }
+    },
+    "has-proto": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="
+    },
     "has-symbols": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
-      "dev": true
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
+    },
+    "has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "peer": true,
+      "requires": {
+        "has-symbols": "^1.0.3"
+      }
     },
     "has-unicode": {
       "version": "2.0.1",
@@ -21834,6 +22290,14 @@
       "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
       "dev": true
     },
+    "hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "requires": {
+        "function-bind": "^1.1.2"
+      }
+    },
     "hdr-histogram-js": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz",
@@ -22232,6 +22696,16 @@
       "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==",
       "dev": true
     },
+    "is-arguments": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+      "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+      "peer": true,
+      "requires": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      }
+    },
     "is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -22255,6 +22729,15 @@
         "has": "^1.0.3"
       }
     },
+    "is-date-object": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+      "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+      "peer": true,
+      "requires": {
+        "has-tostringtag": "^1.0.0"
+      }
+    },
     "is-docker": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -22323,6 +22806,16 @@
       "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
       "dev": true
     },
+    "is-regex": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+      "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+      "peer": true,
+      "requires": {
+        "call-bind": "^1.0.2",
+        "has-tostringtag": "^1.0.0"
+      }
+    },
     "is-stream": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -23642,6 +24135,14 @@
         "tslib": "^2.0.0"
       }
     },
+    "ngx-quill": {
+      "version": "23.0.2",
+      "resolved": "https://registry.npmjs.org/ngx-quill/-/ngx-quill-23.0.2.tgz",
+      "integrity": "sha512-6NoO/J7JWMdUdFRHwLVzyYWvihilrl9zEYUp8rRBm0bNdgMPTBUmoHnMbto8ln/eNu+APOfVq+DboTx8vO1PeA==",
+      "requires": {
+        "tslib": "^2.3.0"
+      }
+    },
     "ngx-webstorage": {
       "version": "12.0.0",
       "resolved": "https://registry.npmjs.org/ngx-webstorage/-/ngx-webstorage-12.0.0.tgz",
@@ -24137,6 +24638,22 @@
       "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
       "dev": true
     },
+    "object-is": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+      "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+      "peer": true,
+      "requires": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1"
+      }
+    },
+    "object-keys": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+      "peer": true
+    },
     "object-path": {
       "version": "0.11.8",
       "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
@@ -24364,6 +24881,11 @@
       "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
       "dev": true
     },
+    "parchment": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
+      "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
+    },
     "parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -24771,6 +25293,45 @@
       "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
     },
+    "quill": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
+      "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
+      "peer": true,
+      "requires": {
+        "clone": "^2.1.1",
+        "deep-equal": "^1.0.1",
+        "eventemitter3": "^2.0.3",
+        "extend": "^3.0.2",
+        "parchment": "^1.1.4",
+        "quill-delta": "^3.6.2"
+      },
+      "dependencies": {
+        "clone": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+          "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+          "peer": true
+        },
+        "eventemitter3": {
+          "version": "2.0.3",
+          "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
+          "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
+          "peer": true
+        }
+      }
+    },
+    "quill-delta": {
+      "version": "3.6.3",
+      "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
+      "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
+      "peer": true,
+      "requires": {
+        "deep-equal": "^1.0.1",
+        "extend": "^3.0.2",
+        "fast-diff": "1.1.2"
+      }
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -24896,6 +25457,18 @@
       "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==",
       "dev": true
     },
+    "regexp.prototype.flags": {
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+      "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
+      "peer": true,
+      "requires": {
+        "call-bind": "^1.0.6",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "set-function-name": "^2.0.1"
+      }
+    },
     "regexpu-core": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz",
@@ -25339,6 +25912,31 @@
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
       "dev": true
     },
+    "set-function-length": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+      "requires": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      }
+    },
+    "set-function-name": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+      "peer": true,
+      "requires": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.2"
+      }
+    },
     "setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -25743,9 +26341,9 @@
       "dev": true
     },
     "tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "dev": true,
       "requires": {
         "chownr": "^2.0.0",
@@ -26095,6 +26693,11 @@
       "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
       "dev": true
     },
+    "uninstall": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/uninstall/-/uninstall-0.0.0.tgz",
+      "integrity": "sha512-pjP/0+A4gsbDVa8XH/S2GZdT9NPJW8NFMy3GI7HnsWG+NAmFSSj3QidNosXBI9cPtxxNExEDdhKFO6sli8K3mA=="
+    },
     "unique-filename": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
diff --git a/ui/package.json b/ui/package.json
index a3ddf84ac..4fa77790e 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -32,9 +32,11 @@
     "jsrsasign-util": "^1.0.5",
     "moment": "^2.29.4",
     "ngx-clipboard": "^16.0.0",
+    "ngx-quill": "^23.0.2",
     "ngx-webstorage": "^12.0.0",
     "rxjs": "~7.5.0",
     "tslib": "^2.3.0",
+    "uninstall": "^0.0.0",
     "zone.js": "~0.13.3"
   },
   "devDependencies": {
@@ -48,6 +50,7 @@
     "@angular/compiler-cli": "^16.2.9",
     "@types/jasmine": "~4.0.0",
     "@types/jsrsasign": "^10.5.13",
+    "@types/quill": "^1.3.10",
     "@typescript-eslint/eslint-plugin": "5.62.0",
     "@typescript-eslint/parser": "5.62.0",
     "eslint": "^8.49.0",
diff --git a/ui/src/app/affiliation/affiliations.component.ts b/ui/src/app/affiliation/affiliations.component.ts
index 510699b6c..5eddaee12 100644
--- a/ui/src/app/affiliation/affiliations.component.ts
+++ b/ui/src/app/affiliation/affiliations.component.ts
@@ -87,6 +87,7 @@ export class AffiliationsComponent implements OnInit, OnDestroy {
       this.searchTerm = ''
       this.submittedSearchTerm = ''
       this.loadAll()
+      console.log('test')
     })
     this.importEventSubscriber = this.eventService.on(EventType.IMPORT_AFFILIATIONS).subscribe(() => {
       this.loadAll()
diff --git a/ui/src/app/home/home.component.scss b/ui/src/app/home/home.component.scss
index b47a816ce..759fef2d0 100644
--- a/ui/src/app/home/home.component.scss
+++ b/ui/src/app/home/home.component.scss
@@ -1,3 +1,4 @@
+@import '~quill/dist/quill.snow.css';
 :host {
   max-width: 1250px;
   display: block;
diff --git a/ui/src/app/home/home.module.ts b/ui/src/app/home/home.module.ts
index b8d49c2b6..db7828823 100644
--- a/ui/src/app/home/home.module.ts
+++ b/ui/src/app/home/home.module.ts
@@ -5,9 +5,12 @@ import { routes } from './home.route'
 import { HomeComponent } from './home.component'
 import { MemberInfoComponent } from './member-info/member-info.component'
 import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'
+import { MemberInfoEditComponent } from './member-info/member-info-edit.component'
+import { QuillModule } from 'ngx-quill'
+import { ReactiveFormsModule } from '@angular/forms'
 
 @NgModule({
-  imports: [CommonModule, RouterModule.forChild(routes), FontAwesomeModule],
-  declarations: [HomeComponent, MemberInfoComponent],
+  imports: [CommonModule, RouterModule.forChild(routes), FontAwesomeModule, QuillModule.forRoot(), ReactiveFormsModule],
+  declarations: [HomeComponent, MemberInfoComponent, MemberInfoEditComponent],
 })
 export class HomeModule {}
diff --git a/ui/src/app/home/home.route.ts b/ui/src/app/home/home.route.ts
index ff6c70412..93bb025da 100644
--- a/ui/src/app/home/home.route.ts
+++ b/ui/src/app/home/home.route.ts
@@ -5,6 +5,7 @@ import { MemberInfoComponent } from './member-info/member-info.component'
 import { Injectable, inject } from '@angular/core'
 import { MemberService } from '../member/service/member.service'
 import { Observable, map } from 'rxjs'
+import { MemberInfoEditComponent } from './member-info/member-info-edit.component'
 
 export const ManageMemberGuard = (route: ActivatedRouteSnapshot): Observable<boolean> | boolean => {
   const router = inject(Router)
@@ -63,6 +64,24 @@ export const routes: Routes = [
         },
         canActivate: [AuthGuard],
       },
+      {
+        path: 'edit',
+        component: MemberInfoEditComponent,
+        data: {
+          authorities: ['ROLE_USER'],
+          pageTitle: 'home.title.string',
+        },
+        canActivate: [AuthGuard, ManageMemberGuard],
+      },
+      {
+        path: 'manage/:id/edit',
+        component: MemberInfoEditComponent,
+        data: {
+          authorities: ['ROLE_USER'],
+          pageTitle: 'home.title.string',
+        },
+        canActivate: [AuthGuard],
+      },
     ],
   },
 ]
diff --git a/ui/src/app/home/member-info/member-info-edit.component.html b/ui/src/app/home/member-info/member-info-edit.component.html
new file mode 100644
index 000000000..d0a5e56b0
--- /dev/null
+++ b/ui/src/app/home/member-info/member-info-edit.component.html
@@ -0,0 +1,440 @@
+<div *ngIf="memberData" class="h-100">
+  <div class="p-3 mb-3">
+    <div class="font-size-14 line-height-150">
+      Back to
+      <a routerLink="" class="font-weight-normal text-decoration-underline">{{ memberData.publicDisplayName }}</a>
+    </div>
+    <hr class="mb-4" />
+    <div class="edit-org-label mb-10 font-size-18 color-gray line-height-150">Edit organization</div>
+    <h1 class="font-weight-bold mb-3 wide-text">{{ memberData.publicDisplayName }}</h1>
+    <div class="line-height-150">Manage public and private information for this organization.</div>
+  </div>
+  <div class="d-flex">
+    <div class="side-bar">
+      <div class="logo-container mb-20">
+        <img
+          src="{{ memberData.logoUrl }}"
+          onerror="this.src='../../content/images/member-logo-placeholder.svg'"
+          alt="Member logo"
+        />
+      </div>
+      <div class="text-center">
+        <img src="./../../../../content/images/lockpad.svg" alt="Locked" />
+      </div>
+      <div class="text-center m-1">
+        <em class="wide-text font-size-12 line-height-150">Logo upload coming soon</em>
+      </div>
+    </div>
+    <div class="main-section">
+      <form (ngSubmit)="save()" name="editForm" role="form" [formGroup]="editForm">
+        <div *ngIf="invalidForm" class="warning d-flex w-75 p-3 mb-3">
+          <img src="./../../../../content/images/error-sign.svg" alt="Warning sign" class="p-2" />
+          <div>
+            <div class="mb-2 font-size-12 wide-text font-weight-bold line-height-150">Your changes cannot be saved</div>
+            <div class="font-size-12 wide-text line-height-150">
+              Please fix the issues with the public details before trying to save again
+            </div>
+          </div>
+        </div>
+        <h2 class="mb-30 wide-text font-size-24">Organization details</h2>
+
+        <!-- Organization name -->
+
+        <h3 class="mb-20 font-size-16 font-weight-bold">Organization name</h3>
+        <div class="form-group mb-30">
+          <input
+            type="text"
+            class="form-control wide-text-25 org-name-input-field"
+            name="orgName"
+            formControlName="orgName"
+            (input)="editForm.get('orgName')?.markAsUntouched()"
+            [ngClass]="{
+              'text-danger':
+                editForm.get('orgName')?.invalid && editForm.get('orgName')?.touched && editForm.get('orgName')?.dirty,
+              'input-field-default-border': !editForm.get('orgName')?.dirty || !editForm.get('orgName')?.touched
+            }"
+          />
+          <ng-template #validOrgName>
+            <small class="wide-text font-size-12 form-text color-gray"
+              >The legal or official name for this organization. Max 41 characters.</small
+            >
+          </ng-template>
+          <div
+            *ngIf="
+              editForm.get('orgName')?.invalid && editForm.get('orgName')?.touched && editForm.get('orgName')?.dirty;
+              else validOrgName
+            "
+          >
+            <small
+              class="wide-text font-size-12 form-text text-danger"
+              *ngIf="(editForm.get('orgName')?.errors)!['required']"
+            >
+              Organization name cannot be empty
+            </small>
+            <div>
+              <small
+                class="wide-text font-size-12 form-text text-danger"
+                *ngIf="(editForm.get('orgName')?.errors)!['maxlength']"
+              >
+                Organization name is too long. Please use 41 characters or less.
+              </small>
+            </div>
+          </div>
+        </div>
+
+        <!-- Billing address -->
+
+        <h3 class="mb-20 font-size-16 font-weight-bold">Billing address</h3>
+        <div class="form-group mb-20">
+          <label
+            class="wide-text font-size-12 font-weight-bold"
+            [ngClass]="{ 'text-danger': editForm.get('street')?.invalid && editForm.get('street')?.touched }"
+            >Street</label
+          >
+          <input
+            type="text"
+            class="form-control wide-text-25 w-75"
+            name="street"
+            formControlName="street"
+            (input)="editForm.get('street')?.markAsUntouched()"
+            [ngClass]="{
+              'text-danger': editForm.get('street')?.invalid && editForm.get('street')?.touched,
+              'input-field-default-border': !editForm.get('street')?.dirty || !editForm.get('street')?.touched
+            }"
+          />
+          <div *ngIf="editForm.get('street')?.invalid && editForm.get('street')?.touched">
+            <small
+              class="wide-text font-size-12 form-text text-danger"
+              *ngIf="(editForm.get('street')?.errors)!['maxlength']"
+            >
+              Street name is too long. Please use 255 characters or less.
+            </small>
+          </div>
+        </div>
+        <div class="form-group mb-20">
+          <label
+            class="wide-text font-size-12 font-weight-bold"
+            [ngClass]="{ 'text-danger': editForm.get('city')?.invalid && editForm.get('city')?.touched }"
+            >City</label
+          >
+          <input
+            type="text"
+            class="form-control wide-text-25 w-75"
+            name="city"
+            formControlName="city"
+            (input)="editForm.get('city')?.markAsUntouched()"
+            [ngClass]="{
+              'text-danger': editForm.get('city')?.invalid && editForm.get('city')?.touched,
+              'input-field-default-border': !editForm.get('city')?.dirty || !editForm.get('city')?.touched
+            }"
+          />
+          <div *ngIf="editForm.get('city')?.invalid && editForm.get('city')?.touched">
+            <small
+              class="wide-text font-size-12 form-text text-danger"
+              *ngIf="(editForm.get('city')?.errors)!['maxlength']"
+            >
+              City name is too long. Please use 40 characters or less.
+            </small>
+          </div>
+        </div>
+        <div class="form-group mb-20" *ngIf="states">
+          <label
+            class="wide-text font-size-12 font-weight-bold"
+            [ngClass]="{
+              'text-danger': editForm.get('state')?.invalid && editForm.get('state')?.touched
+            }"
+            >State/Province</label
+          >
+          <select
+            class="form-control font-size-14 wide-text-25 w-75"
+            name="state"
+            formControlName="state"
+            [ngClass]="{
+              'text-danger': editForm.get('state')?.invalid && editForm.get('state')?.dirty,
+              'input-field-default-border': !editForm.get('state')?.dirty
+            }"
+          >
+            <option selected [value]="null">-- No state or province --</option>
+            <option *ngFor="let state of states" [value]="state.name" class="form-field-text-color-default">
+              {{ state.name }}
+            </option>
+          </select>
+        </div>
+        <div class="form-group mb-20">
+          <label
+            class="wide-text font-size-12 font-weight-bold"
+            [ngClass]="{ 'text-danger': editForm.get('country')?.invalid && editForm.get('country')?.dirty }"
+            >Country</label
+          >
+          <input
+            readonly
+            class="form-control font-size-14 wide-text-25 w-75"
+            name="country"
+            formControlName="country"
+          />
+        </div>
+        <div class="form-group mb-30">
+          <label
+            class="wide-text font-size-12 font-weight-bold"
+            [ngClass]="{ 'text-danger': editForm.get('postcode')?.invalid && editForm.get('postcode')?.touched }"
+            >ZIP/Postcode</label
+          >
+          <input
+            type="text"
+            class="form-control wide-text-25 postcode-input-field"
+            name="postcode"
+            formControlName="postcode"
+            (input)="editForm.get('postcode')?.markAsUntouched()"
+            [ngClass]="{
+              'text-danger': editForm.get('postcode')?.invalid && editForm.get('postcode')?.touched,
+              'input-field-default-border': !editForm.get('postcode')?.dirty || !editForm.get('postcode')?.touched
+            }"
+          />
+          <div *ngIf="editForm.get('postcode')?.invalid && editForm.get('postcode')?.touched">
+            <small
+              class="wide-text font-size-12 form-text text-danger"
+              *ngIf="(editForm.get('postcode')?.errors)!['maxlength']"
+            >
+              ZIP/Postcode is too long. Please use 20 characters or less.
+            </small>
+          </div>
+        </div>
+
+        <!-- Trademark license -->
+
+        <h3 class="font-weight-bold font-size-16 mb-10">Trademark license</h3>
+        <div class="font-size-14 wide-text-25 mb-20 line-height-150">
+          Can ORCID use this organization's trademarked name and logos?
+        </div>
+        <div class="mb-40">
+          <div class="form-group d-flex">
+            <input
+              type="radio"
+              id="trademarkLicenseYes"
+              class="form-control radio mr-8"
+              name="trademarkLicense"
+              value="Yes"
+              formControlName="trademarkLicense"
+              [ngClass]="{
+                'outline-danger': editForm.get('trademarkLicense')?.invalid && editForm.get('trademarkLicense')?.dirty
+              }"
+            />
+            <label for="trademarkLicenseYes" class="wide-text-25 font-size-14 font-weight-normal"
+              ><strong>YES</strong> - ORCID can use this organization's trademarked name and logos</label
+            >
+          </div>
+          <div class="form-group d-flex">
+            <input
+              type="radio"
+              id="trademarkLicenseNo"
+              class="form-control radio mr-8"
+              name="trademarkLicense"
+              value="No"
+              formControlName="trademarkLicense"
+              [ngClass]="{
+                'outline-danger': editForm.get('trademarkLicense')?.invalid && editForm.get('trademarkLicense')?.dirty
+              }"
+            />
+            <label for="trademarkLicenseNo" class="wide-text-25 font-size-14 font-weight-normal"
+              ><strong>NO</strong> - ORCID cannot use this organization's trademarked name and logos</label
+            >
+          </div>
+          <div *ngIf="editForm.get('trademarkLicense')?.invalid && editForm.get('trademarkLicense')?.dirty">
+            <small
+              class="wide-text font-size-12 form-text text-danger"
+              *ngIf="(editForm.get('trademarkLicense')?.errors)!['requred']"
+            >
+              Please select a trademark license option
+            </small>
+          </div>
+        </div>
+
+        <!-- PUBLIC DETAILS -->
+
+        <h2 class="wide-text font-size-24">Public details</h2>
+        <div class="wide-text-25 mb-20 font-size-14 line-height-150">
+          Organization information to be displayed publicly, such as on the
+          <a href="{{ MEMBER_LIST_URL }} " target="_blank">ORCID member list</a>
+        </div>
+        <div class="mb-5">
+          <div class="form-group w-75">
+            <label
+              class="wide-text font-size-12"
+              [ngClass]="{
+                'text-danger':
+                  editForm.get('publicName')?.invalid &&
+                  editForm.get('publicName')?.touched &&
+                  editForm.get('publicName')?.dirty
+              }"
+              >Public display name</label
+            >
+            <input
+              type="text"
+              class="form-control"
+              name="publicName"
+              (input)="editForm.get('publicName')?.markAsUntouched()"
+              formControlName="publicName"
+              [ngClass]="{
+                'text-danger':
+                  editForm.get('publicName')?.invalid &&
+                  editForm.get('publicName')?.touched &&
+                  editForm.get('publicName')?.dirty,
+                'input-field-default-border': !editForm.get('publicName')?.touched || !editForm.get('publicName')?.dirty
+              }"
+            />
+            <div *ngIf="editForm.get('publicName')?.invalid">
+              <small
+                class="wide-text font-size-12 form-text text-danger"
+                *ngIf="(editForm.get('publicName')?.errors)!['requred']"
+              >
+                Public organization name cannot be empty
+              </small>
+              <div>
+                <small
+                  class="wide-text font-size-12 form-text text-danger"
+                  *ngIf="(editForm.get('publicName')?.errors)!['maxlength']"
+                >
+                  Public organization name is too long. Please use 255 characters or less.
+                </small>
+              </div>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="wide-text font-size-12">Organization description</label>
+            <quill-editor
+              class="d-block w-75 description"
+              formControlName="description"
+              format="html"
+              [styles]="quillStyles"
+              [modules]="quillConfig"
+              [placeholder]="'A brief description of the organization'"
+            >
+            </quill-editor>
+            <div *ngIf="editForm.get('description')?.invalid">
+              <small
+                class="wide-text font-size-12 form-text text-danger"
+                *ngIf="(editForm.get('description')?.errors)!['maxlength']"
+              >
+                Organization description is too long. Please use 5000 characters or less.
+              </small>
+            </div>
+          </div>
+          <div class="form-group w-75">
+            <label
+              class="wide-text font-size-12"
+              [ngClass]="{
+                'text-danger':
+                  editForm.get('website')?.invalid && editForm.get('website')?.touched && editForm.get('website')?.dirty
+              }"
+              >Website</label
+            >
+            <input
+              type="text"
+              class="form-control"
+              name="website"
+              (input)="editForm.get('website')?.markAsUntouched()"
+              formControlName="website"
+              [ngClass]="{
+                'text-danger':
+                  editForm.get('website')?.invalid &&
+                  editForm.get('website')?.touched &&
+                  editForm.get('website')?.dirty,
+                'input-field-default-border': !editForm.get('website')?.touched || !editForm.get('website')?.dirty
+              }"
+            />
+            <ng-template #validWebsite>
+              <small class="wide-text font-size-12 color-gray"
+                >Links should be in the full URL format e.g. http://www.website.com</small
+              >
+            </ng-template>
+            <div *ngIf="editForm.get('website')?.invalid && editForm.get('website')?.touched; else validWebsite">
+              <small class="wide-text font-size-12 text-danger" *ngIf="(editForm.get('website')?.errors)!['pattern']">
+                Please enter a valid website URL, for example http://www.website.com
+              </small>
+              <div>
+                <small
+                  class="wide-text font-size-12 text-danger"
+                  *ngIf="(editForm.get('website')?.errors)!['maxlength']"
+                >
+                  Website is too long. Please use 255 characters or less.
+                </small>
+              </div>
+            </div>
+          </div>
+          <div class="form-group mb-40 w-75">
+            <label
+              [ngClass]="{ 'text-danger': editForm.get('email')?.invalid && editForm.get('email')?.touched }"
+              class="wide-text font-size-12"
+              >Email</label
+            >
+            <input
+              type="text"
+              class="form-control"
+              name="email"
+              (input)="editForm.get('email')?.markAsUntouched()"
+              formControlName="email"
+              [ngClass]="{
+                'text-danger': editForm.get('email')?.invalid && editForm.get('email')?.touched,
+                'input-field-default-border': !editForm.get('email')?.touched
+              }"
+            />
+            <ng-template #validEmail>
+              <small class="wide-text font-size-12 color-gray"
+                >Emails should be in the standard format e.g. contactus&#64;website.com</small
+              >
+            </ng-template>
+            <div *ngIf="editForm.get('email')?.invalid && editForm.get('email')?.touched; else validEmail">
+              <small class="wide-text font-size-12 text-danger" *ngIf="(editForm.get('email')?.errors)!['pattern']">
+                Please enter a valid email address, for example contactus&#64;website.com
+              </small>
+              <div>
+                <small class="wide-text font-size-12 text-danger" *ngIf="(editForm.get('email')?.errors)!['maxlength']">
+                  Email is too long. Please use 80 characters or less.
+                </small>
+              </div>
+            </div>
+          </div>
+          <button type="submit" [disabled]="isSaving" class="btn btn-primary">Save changes</button>
+          <button type="button" class="btn btn-outline-primary" routerLink="">Cancel</button>
+        </div>
+      </form>
+      <div *ngIf="memberData.orgIds" class="mb-40">
+        <div class="d-flex mb-2">
+          <h3 class="mb-0 mr-2">Identifiers</h3>
+        </div>
+        <div class="wide-text font-size-12 coming-soon line-height-150">
+          <em
+            >We did our best to register the right organization identifiers in our systems when you became an ORCID
+            member. If you think any are missing (ROR, GRID, RINGGOLD or Funder Registry ID)
+            <a href="mailto:membership@orcid.org">contact your engagement support lead</a> to register additional
+            IDs</em
+          >
+        </div>
+        <div class="row ml-0 d-flex justify-content-between contact">
+          <h4 class="w-66 font-size-14">ID</h4>
+          <h4 class="w-33 font-size-14">Type</h4>
+        </div>
+        <hr class="green-hr mb-0" />
+        <ul class="ml-0 pl-0 mb-0" *ngFor="let orgId of orgIdsTransformed | keyvalue; let i = index">
+          <li class="row ml-0 pt-16 pb-16 d-flex justify-content-between contact">
+            <div class="w-66 line-height-150">{{ orgId.value }}</div>
+            <div class="w-33 line-height-150">{{ orgId.key }}</div>
+          </li>
+          <hr *ngIf="!(i == objectKeys(memberData.orgIds).length - 1)" class="mb-0 mt-0" />
+        </ul>
+      </div>
+      <div *ngIf="memberData.isConsortiumLead">
+        <h3 class="mb-4 font-weight-bold">
+          Consortium Members
+          <span class="font-weight-normal">({{ memberData.consortiumMembers?.length }})</span>
+        </h3>
+        <h4 class="font-size-14">Member name</h4>
+        <hr class="green-hr" />
+        <div *ngFor="let consortiumMember of memberData.consortiumMembers; let i = index">
+          <div>{{ consortiumMember.orgName }}</div>
+          <hr *ngIf="memberData.consortiumMembers && i + 1 < memberData.consortiumMembers!.length" />
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
diff --git a/ui/src/app/home/member-info/member-info-edit.component.scss b/ui/src/app/home/member-info/member-info-edit.component.scss
new file mode 100644
index 000000000..bc8539101
--- /dev/null
+++ b/ui/src/app/home/member-info/member-info-edit.component.scss
@@ -0,0 +1,130 @@
+@use '../../../content/scss/bootstrap-variables' as global;
+
+button,
+textarea,
+input,
+span,
+h1,
+h2,
+h3,
+h4,
+label,
+small {
+  line-height: 150%;
+}
+
+button {
+  font-size: 14px;
+}
+
+.side-bar {
+  max-width: 300px;
+  height: 100%;
+  padding: 1.25rem;
+  flex: 1;
+}
+
+.logo-container {
+  img {
+    width: 200px;
+    height: auto;
+    align-self: center;
+  }
+  width: 208px;
+  height: 208px;
+  display: flex;
+  justify-content: center;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.contact {
+  > div,
+  h4 {
+    width: 10%;
+    text-align: left;
+    flex-grow: 1;
+    max-width: 200px;
+    font-size: 14px;
+  }
+  word-wrap: break-word;
+}
+
+.main-section {
+  label {
+    font-weight: bold;
+    margin-bottom: 4px;
+  }
+  flex: 1;
+  padding: 1.25rem;
+}
+
+.input-prompt {
+  color: global.$black-transparent;
+}
+
+textarea {
+  min-height: 9rem;
+  font-size: 14px;
+  letter-spacing: 0.25px;
+}
+
+input {
+  font-size: 14px;
+  letter-spacing: 0.25px;
+}
+
+input,
+select,
+option {
+  height: 40px;
+}
+
+.coming-soon {
+  margin-bottom: 24px;
+}
+
+.lockpad {
+  margin-top: -4px;
+}
+
+.ng-invalid:not(form) {
+  border: 1px solid #d32f2f;
+  border-radius: 2px;
+}
+
+.warning {
+  img {
+    padding: 0 16px 0 8px !important;
+    margin-top: -1.75rem;
+  }
+  border: 2px solid #b71c1c;
+  border-radius: 4px;
+}
+
+.radio {
+  height: 1.125rem;
+  width: 1.125rem;
+  accent-color: global.$info;
+}
+
+.postcode-input-field {
+  max-width: 180px;
+}
+
+.org-name-input-field {
+  max-width: 400px;
+}
+
+:host ::ng-deep .ql-editor a {
+  color: global.$info;
+  text-decoration: none;
+}
+
+:host ::ng-deep .ql-toolbar button:hover .ql-stroke {
+  stroke: global.$info !important;
+}
+
+:host ::ng-deep .ql-active .ql-stroke {
+  stroke: global.$info !important;
+}
diff --git a/ui/src/app/home/member-info/member-info-edit.component.spec.ts b/ui/src/app/home/member-info/member-info-edit.component.spec.ts
new file mode 100644
index 000000000..447862b83
--- /dev/null
+++ b/ui/src/app/home/member-info/member-info-edit.component.spec.ts
@@ -0,0 +1,21 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { MemberInfoEditComponent } from './member-info-edit.component';
+
+describe('MemberInfoEditComponent', () => {
+  let component: MemberInfoEditComponent;
+  let fixture: ComponentFixture<MemberInfoEditComponent>;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      declarations: [MemberInfoEditComponent]
+    });
+    fixture = TestBed.createComponent(MemberInfoEditComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/ui/src/app/home/member-info/member-info-edit.component.ts b/ui/src/app/home/member-info/member-info-edit.component.ts
new file mode 100644
index 000000000..08f95af49
--- /dev/null
+++ b/ui/src/app/home/member-info/member-info-edit.component.ts
@@ -0,0 +1,227 @@
+import { KeyValue } from '@angular/common'
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { FormBuilder, FormControl, Validators } from '@angular/forms'
+import { ActivatedRoute, Router } from '@angular/router'
+import { EMPTY, Subject, combineLatest } from 'rxjs'
+import { switchMap, take, takeUntil } from 'rxjs/operators'
+import { AccountService } from 'src/app/account'
+import { IAccount } from 'src/app/account/model/account.model'
+import { EMAIL_REGEXP, URL_REGEXP } from 'src/app/app.constants'
+import { ISFAddress } from 'src/app/member/model/salesforce-address.model'
+import { ISFCountry } from 'src/app/member/model/salesforce-country.model'
+import { ISFState } from 'src/app/member/model/salesforce-country.model copy'
+import { ISFMemberData } from 'src/app/member/model/salesforce-member-data.model'
+import { ISFMemberUpdate, SFMemberUpdate } from 'src/app/member/model/salesforce-member-update.model'
+import { MemberService } from 'src/app/member/service/member.service'
+
+@Component({
+  selector: 'app-member-info-edit',
+  templateUrl: './member-info-edit.component.html',
+  styleUrls: ['./member-info-edit.component.scss'],
+})
+export class MemberInfoEditComponent implements OnInit, OnDestroy {
+  countries: ISFCountry[] | undefined
+  country: ISFCountry | undefined
+  states: ISFState[] | undefined
+  account: IAccount | undefined | null
+  memberData: ISFMemberData | undefined | null
+  objectKeys = Object.keys
+  orgIdsTransformed: KeyValue<string, string[]>[] = []
+  // TODO move to constants
+  MEMBER_LIST_URL: string = 'https://orcid.org/members'
+  isSaving = false
+  invalidForm: boolean | undefined
+  managedMember: string | undefined
+  destroy$ = new Subject()
+  quillConfig = {
+    toolbar: [['bold', 'italic'], [{ list: 'ordered' }, { list: 'bullet' }], ['link']],
+  }
+  quillStyles = {
+    fontFamily: 'inherit',
+    fontSize: '14px',
+    letterSpacing: '0.25px',
+    marginRight: '0',
+  }
+
+  editForm = this.fb.group({
+    orgName: new FormControl<null | string>(null, [Validators.required, Validators.maxLength(41)]),
+    street: new FormControl<null | string>(null, [Validators.maxLength(255)]),
+    city: new FormControl<null | string>(null, [Validators.maxLength(40)]),
+    state: new FormControl<null | string>(null, [Validators.maxLength(80)]),
+    country: new FormControl<null | string>(null, [Validators.required]),
+    postcode: new FormControl<null | string>(null, [Validators.maxLength(20)]),
+    trademarkLicense: new FormControl<null | string>(null, [Validators.required]),
+    publicName: new FormControl<null | string>(null, [Validators.required, Validators.maxLength(255)]),
+    description: new FormControl<null | string>(null, [Validators.maxLength(5000)]),
+    website: new FormControl<null | string>(null, [Validators.pattern(URL_REGEXP), Validators.maxLength(255)]),
+    email: new FormControl<null | string>(null, [Validators.pattern(EMAIL_REGEXP), Validators.maxLength(80)]),
+  })
+
+  constructor(
+    private memberService: MemberService,
+    private accountService: AccountService,
+    private fb: FormBuilder,
+    protected activatedRoute: ActivatedRoute,
+    private router: Router
+  ) {}
+
+  ngOnInit() {
+    combineLatest([this.activatedRoute.params, this.accountService.getAccountData()])
+      .pipe(
+        switchMap(([params, account]) => {
+          if (params['id']) {
+            this.managedMember = params['id']
+          }
+          if (account) {
+            this.account = account
+            if (this.managedMember) {
+              this.memberService.setManagedMember(params['id'])
+              return this.memberService.getMemberData(this.managedMember)
+            } else {
+              return this.memberService.getMemberData(account?.salesforceId)
+            }
+          } else {
+            return EMPTY
+          }
+        }),
+        takeUntil(this.destroy$)
+      )
+      .subscribe((data) => {
+        this.memberData = data
+        this.orgIdsTransformed = Object.entries(this.memberData?.orgIds || {}).map(([key, value]) => ({ key, value }))
+        this.validateUrl()
+        if (data) {
+          this.updateForm(data)
+        }
+      })
+    this.memberService
+      .getCountries()
+      .pipe(take(1))
+      .subscribe((countries) => {
+        this.countries = countries
+        if (this.memberData) {
+          this.updateForm(this.memberData)
+        }
+      })
+
+    this.editForm.valueChanges.subscribe(() => {
+      if (this.editForm.status === 'VALID') {
+        this.invalidForm = false
+      }
+    })
+  }
+
+  validateUrl() {
+    if (this.memberData?.website && !/(http(s?)):\/\//i.test(this.memberData.website)) {
+      this.memberData.website = 'http://' + this.memberData.website
+    }
+  }
+
+  updateForm(data: ISFMemberData) {
+    if (data && data.id) {
+      this.editForm.patchValue({
+        orgName: data.name || null,
+        trademarkLicense: data.trademarkLicense ? data.trademarkLicense : 'No',
+        publicName: data.publicDisplayName,
+        description: data.publicDisplayDescriptionHtml,
+        website: data.website,
+        email: data.publicDisplayEmail,
+      })
+      if (data.billingAddress) {
+        if (this.countries) {
+          this.country = this.countries.find((country) => country.name === data.billingAddress?.country)
+          if (this.country) {
+            this.states = this.country.states
+          } else {
+            console.error('Unable to find country: ', data.billingAddress.country)
+          }
+        }
+        this.editForm.patchValue({
+          street: data.billingAddress.street,
+          city: data.billingAddress.city,
+          state: data.billingAddress.state,
+          country: data.billingAddress.country,
+          postcode: data.billingAddress.postalCode,
+        })
+      }
+    }
+  }
+
+  filterCRFID(id: string) {
+    return id.replace(/^.*dx.doi.org\//g, '')
+  }
+
+  createDetailsFromForm(): ISFMemberUpdate {
+    const address: ISFAddress = {
+      street: this.editForm.get(['street'])?.value,
+      city: this.editForm.get(['city'])?.value,
+      state:
+        this.editForm.get(['state'])?.value == '-- No state or province --'
+          ? undefined
+          : this.editForm.get(['state'])?.value,
+      country: this.editForm.get(['country'])?.value,
+      countryCode: this.country?.code,
+      postalCode: this.editForm.get(['postcode'])?.value,
+    }
+    return {
+      ...new SFMemberUpdate(),
+      orgName: this.editForm.get(['orgName'])?.value,
+      billingAddress: address,
+      trademarkLicense: this.editForm.get(['trademarkLicense'])?.value,
+      publicName: this.editForm.get(['publicName'])?.value,
+      description: this.editForm.get(['description'])?.value,
+      website: this.editForm.get(['website'])?.value,
+      email: this.editForm.get(['email'])?.value,
+    }
+  }
+
+  save() {
+    if (this.editForm.status === 'INVALID') {
+      this.invalidForm = true
+      this.editForm.markAllAsTouched()
+      Object.keys(this.editForm.controls).forEach((key) => {
+        this.editForm.get(key)?.markAsDirty()
+      })
+    } else {
+      this.invalidForm = false
+      this.isSaving = true
+      const details: ISFMemberUpdate = this.createDetailsFromForm()
+
+      if (this.memberData?.id) {
+        this.memberService.updateMemberDetails(details, this.memberData?.id).subscribe({
+          next: () => {
+            this.memberService.setMemberData({
+              ...this.memberData,
+              publicDisplayDescriptionHtml: details.description,
+              publicDisplayName: details.publicName,
+              name: details.orgName,
+              billingAddress: details.billingAddress,
+              trademarkLicense: details.trademarkLicense,
+              publicDisplayEmail: details.email,
+              website: details.website,
+            })
+            this.onSaveSuccess()
+          },
+          error: (err: any) => {
+            console.error(err)
+            this.onSaveError()
+          },
+        })
+      }
+    }
+  }
+
+  ngOnDestroy() {
+    this.destroy$.next(true)
+    this.destroy$.complete()
+  }
+
+  onSaveSuccess() {
+    this.isSaving = false
+    this.router.navigate([''])
+  }
+
+  onSaveError() {
+    this.isSaving = false
+  }
+}
diff --git a/ui/src/app/home/member-info/member-info.component.ts b/ui/src/app/home/member-info/member-info.component.ts
index 52d1f9152..3b36d3929 100644
--- a/ui/src/app/home/member-info/member-info.component.ts
+++ b/ui/src/app/home/member-info/member-info.component.ts
@@ -49,7 +49,6 @@ export class MemberInfoComponent implements OnInit, OnDestroy {
           if (params['id']) {
             this.managedMember = params['id']
           }
-
           if (account) {
             this.account = account
             if (this.managedMember) {
diff --git a/ui/src/app/member/service/member.service.ts b/ui/src/app/member/service/member.service.ts
index 87571aeb3..3dfd5e06f 100644
--- a/ui/src/app/member/service/member.service.ts
+++ b/ui/src/app/member/service/member.service.ts
@@ -29,6 +29,7 @@ import {
 import { ISFCountry } from '../model/salesforce-country.model'
 import { ISFRawMemberContact, ISFRawMemberContacts, SFMemberContact } from '../model/salesforce-member-contact.model'
 import { ISFRawMemberOrgIds, SFMemberOrgIds } from '../model/salesforce-member-org-id.model'
+import { ISFMemberUpdate } from '../model/salesforce-member-update.model'
 
 @Injectable({ providedIn: 'root' })
 export class MemberService {
@@ -145,14 +146,18 @@ export class MemberService {
     return this.memberData.asObservable()
   }
 
+  setMemberData(memberData: ISFMemberData | undefined | null) {
+    this.memberData.next(memberData)
+  }
+
   private fetchMemberData(salesforceId: string) {
     this.fetchingMemberDataState = true
-    this.getSFMemberData(salesforceId)
+    this.fetchSFMemberData(salesforceId)
       .pipe(
         switchMap((res) => {
           this.memberData.next(res)
           return combineLatest([
-            this.getMemberContacts(salesforceId),
+            this.fetchMemberContacts(salesforceId),
             this.getMemberOrgIds(salesforceId),
             this.getConsortiaLeadName(res.consortiaLeadId!),
             this.getIsConsortiumLead(salesforceId),
@@ -170,7 +175,33 @@ export class MemberService {
       .subscribe()
   }
 
-  getMemberContacts(salesforceId: string): Observable<SFMemberContact[]> {
+  getCountries(): Observable<ISFCountry[] | undefined> {
+    if (!this.countries.value) {
+      return this.fetchCountries()
+    }
+    return this.countries.asObservable()
+  }
+
+  fetchCountries(): Observable<ISFCountry[]> {
+    return this.http.get<ISFCountry[]>(`${this.resourceUrl}/countries`).pipe(
+      catchError((error) => {
+        return of('An error occurred:', error)
+      }),
+      tap((res: ISFCountry[]) => {
+        if (res) {
+          this.countries.next(res)
+        } else {
+          console.error('Request failed:', res)
+        }
+      })
+    )
+  }
+
+  updateMemberDetails(memberDetails: ISFMemberUpdate, salesforceId: string): Observable<ISFMemberUpdate> {
+    return this.http.put(`${this.resourceUrl}/members/${salesforceId}/member-details`, memberDetails)
+  }
+
+  private fetchMemberContacts(salesforceId: string): Observable<SFMemberContact[]> {
     return this.http
       .get<ISFRawMemberContacts>(`${this.resourceUrl}/members/${salesforceId}/member-contacts`, { observe: 'response' })
       .pipe(
@@ -183,7 +214,7 @@ export class MemberService {
       )
   }
 
-  getSFMemberData(salesforceId: string): Observable<SFMemberData> {
+  private fetchSFMemberData(salesforceId: string): Observable<SFMemberData> {
     return this.http.get<ISFRawMemberData>(`${this.resourceUrl}/members/${salesforceId}/member-details`).pipe(
       catchError((err) => {
         return of(err)
diff --git a/ui/src/index.html b/ui/src/index.html
index bee93e114..d37623d15 100644
--- a/ui/src/index.html
+++ b/ui/src/index.html
@@ -12,8 +12,6 @@
     <link rel="shortcut icon" href="favicon.ico" />
     <link rel="manifest" href="manifest.webapp" />
     <link rel="stylesheet" href="content/css/loading.css" />
-    <link rel="stylesheet" href="content/css/quill.snow.css" />
-    <link rel="stylesheet" href="content/css/quill.core.css" />
     <link
       href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"
       rel="stylesheet"

From 8365a0269d2e6e145222fedd138d51dd88c28f5d Mon Sep 17 00:00:00 2001
From: andrej romanov <50377758+auumgn@users.noreply.github.com>
Date: Tue, 23 Apr 2024 23:57:14 +0300
Subject: [PATCH 2/3] lint

---
 .../member-info-edit.component.html           | 223 +++++++++---------
 .../member-info/member-info-edit.component.ts |   6 +-
 ui/src/app/user/users.component.ts            |   5 +-
 3 files changed, 118 insertions(+), 116 deletions(-)

diff --git a/ui/src/app/home/member-info/member-info-edit.component.html b/ui/src/app/home/member-info/member-info-edit.component.html
index d0a5e56b0..d922d3d08 100644
--- a/ui/src/app/home/member-info/member-info-edit.component.html
+++ b/ui/src/app/home/member-info/member-info-edit.component.html
@@ -89,19 +89,19 @@ <h3 class="mb-20 font-size-16 font-weight-bold">Billing address</h3>
           <label
             class="wide-text font-size-12 font-weight-bold"
             [ngClass]="{ 'text-danger': editForm.get('street')?.invalid && editForm.get('street')?.touched }"
-            >Street</label
+            ><input
+              type="text"
+              class="form-control wide-text-25 w-75"
+              name="street"
+              formControlName="street"
+              (input)="editForm.get('street')?.markAsUntouched()"
+              [ngClass]="{
+                'text-danger': editForm.get('street')?.invalid && editForm.get('street')?.touched,
+                'input-field-default-border': !editForm.get('street')?.dirty || !editForm.get('street')?.touched
+              }"
+            />Street</label
           >
-          <input
-            type="text"
-            class="form-control wide-text-25 w-75"
-            name="street"
-            formControlName="street"
-            (input)="editForm.get('street')?.markAsUntouched()"
-            [ngClass]="{
-              'text-danger': editForm.get('street')?.invalid && editForm.get('street')?.touched,
-              'input-field-default-border': !editForm.get('street')?.dirty || !editForm.get('street')?.touched
-            }"
-          />
+
           <div *ngIf="editForm.get('street')?.invalid && editForm.get('street')?.touched">
             <small
               class="wide-text font-size-12 form-text text-danger"
@@ -115,19 +115,19 @@ <h3 class="mb-20 font-size-16 font-weight-bold">Billing address</h3>
           <label
             class="wide-text font-size-12 font-weight-bold"
             [ngClass]="{ 'text-danger': editForm.get('city')?.invalid && editForm.get('city')?.touched }"
-            >City</label
+            ><input
+              type="text"
+              class="form-control wide-text-25 w-75"
+              name="city"
+              formControlName="city"
+              (input)="editForm.get('city')?.markAsUntouched()"
+              [ngClass]="{
+                'text-danger': editForm.get('city')?.invalid && editForm.get('city')?.touched,
+                'input-field-default-border': !editForm.get('city')?.dirty || !editForm.get('city')?.touched
+              }"
+            />City</label
           >
-          <input
-            type="text"
-            class="form-control wide-text-25 w-75"
-            name="city"
-            formControlName="city"
-            (input)="editForm.get('city')?.markAsUntouched()"
-            [ngClass]="{
-              'text-danger': editForm.get('city')?.invalid && editForm.get('city')?.touched,
-              'input-field-default-border': !editForm.get('city')?.dirty || !editForm.get('city')?.touched
-            }"
-          />
+
           <div *ngIf="editForm.get('city')?.invalid && editForm.get('city')?.touched">
             <small
               class="wide-text font-size-12 form-text text-danger"
@@ -143,53 +143,51 @@ <h3 class="mb-20 font-size-16 font-weight-bold">Billing address</h3>
             [ngClass]="{
               'text-danger': editForm.get('state')?.invalid && editForm.get('state')?.touched
             }"
+            ><select
+              class="form-control font-size-14 wide-text-25 w-75"
+              name="state"
+              formControlName="state"
+              [ngClass]="{
+                'text-danger': editForm.get('state')?.invalid && editForm.get('state')?.dirty,
+                'input-field-default-border': !editForm.get('state')?.dirty
+              }"
+            >
+              <option selected [value]="null">-- No state or province --</option>
+              <option *ngFor="let state of states" [value]="state.name" class="form-field-text-color-default">
+                {{ state.name }}
+              </option></select
             >State/Province</label
           >
-          <select
-            class="form-control font-size-14 wide-text-25 w-75"
-            name="state"
-            formControlName="state"
-            [ngClass]="{
-              'text-danger': editForm.get('state')?.invalid && editForm.get('state')?.dirty,
-              'input-field-default-border': !editForm.get('state')?.dirty
-            }"
-          >
-            <option selected [value]="null">-- No state or province --</option>
-            <option *ngFor="let state of states" [value]="state.name" class="form-field-text-color-default">
-              {{ state.name }}
-            </option>
-          </select>
         </div>
         <div class="form-group mb-20">
           <label
             class="wide-text font-size-12 font-weight-bold"
             [ngClass]="{ 'text-danger': editForm.get('country')?.invalid && editForm.get('country')?.dirty }"
-            >Country</label
+            ><input
+              readonly
+              class="form-control font-size-14 wide-text-25 w-75"
+              name="country"
+              formControlName="country"
+            />Country</label
           >
-          <input
-            readonly
-            class="form-control font-size-14 wide-text-25 w-75"
-            name="country"
-            formControlName="country"
-          />
         </div>
         <div class="form-group mb-30">
           <label
             class="wide-text font-size-12 font-weight-bold"
             [ngClass]="{ 'text-danger': editForm.get('postcode')?.invalid && editForm.get('postcode')?.touched }"
-            >ZIP/Postcode</label
+            ><input
+              type="text"
+              class="form-control wide-text-25 postcode-input-field"
+              name="postcode"
+              formControlName="postcode"
+              (input)="editForm.get('postcode')?.markAsUntouched()"
+              [ngClass]="{
+                'text-danger': editForm.get('postcode')?.invalid && editForm.get('postcode')?.touched,
+                'input-field-default-border': !editForm.get('postcode')?.dirty || !editForm.get('postcode')?.touched
+              }"
+            />ZIP/Postcode</label
           >
-          <input
-            type="text"
-            class="form-control wide-text-25 postcode-input-field"
-            name="postcode"
-            formControlName="postcode"
-            (input)="editForm.get('postcode')?.markAsUntouched()"
-            [ngClass]="{
-              'text-danger': editForm.get('postcode')?.invalid && editForm.get('postcode')?.touched,
-              'input-field-default-border': !editForm.get('postcode')?.dirty || !editForm.get('postcode')?.touched
-            }"
-          />
+
           <div *ngIf="editForm.get('postcode')?.invalid && editForm.get('postcode')?.touched">
             <small
               class="wide-text font-size-12 form-text text-danger"
@@ -254,7 +252,7 @@ <h3 class="font-weight-bold font-size-16 mb-10">Trademark license</h3>
         <h2 class="wide-text font-size-24">Public details</h2>
         <div class="wide-text-25 mb-20 font-size-14 line-height-150">
           Organization information to be displayed publicly, such as on the
-          <a href="{{ MEMBER_LIST_URL }} " target="_blank">ORCID member list</a>
+          <a href="{{ ORCID_BASE_URL }}/members" target="_blank">ORCID member list</a>
         </div>
         <div class="mb-5">
           <div class="form-group w-75">
@@ -266,22 +264,23 @@ <h2 class="wide-text font-size-24">Public details</h2>
                   editForm.get('publicName')?.touched &&
                   editForm.get('publicName')?.dirty
               }"
-              >Public display name</label
+              ><input
+                type="text"
+                class="form-control"
+                name="publicName"
+                (input)="editForm.get('publicName')?.markAsUntouched()"
+                formControlName="publicName"
+                [ngClass]="{
+                  'text-danger':
+                    editForm.get('publicName')?.invalid &&
+                    editForm.get('publicName')?.touched &&
+                    editForm.get('publicName')?.dirty,
+                  'input-field-default-border':
+                    !editForm.get('publicName')?.touched || !editForm.get('publicName')?.dirty
+                }"
+              />Public display name</label
             >
-            <input
-              type="text"
-              class="form-control"
-              name="publicName"
-              (input)="editForm.get('publicName')?.markAsUntouched()"
-              formControlName="publicName"
-              [ngClass]="{
-                'text-danger':
-                  editForm.get('publicName')?.invalid &&
-                  editForm.get('publicName')?.touched &&
-                  editForm.get('publicName')?.dirty,
-                'input-field-default-border': !editForm.get('publicName')?.touched || !editForm.get('publicName')?.dirty
-              }"
-            />
+
             <div *ngIf="editForm.get('publicName')?.invalid">
               <small
                 class="wide-text font-size-12 form-text text-danger"
@@ -300,16 +299,20 @@ <h2 class="wide-text font-size-24">Public details</h2>
             </div>
           </div>
           <div class="form-group">
-            <label class="wide-text font-size-12">Organization description</label>
-            <quill-editor
-              class="d-block w-75 description"
-              formControlName="description"
-              format="html"
-              [styles]="quillStyles"
-              [modules]="quillConfig"
-              [placeholder]="'A brief description of the organization'"
+            <!-- eslint-disable-next-line -->
+            <label class="wide-text font-size-12"
+              ><quill-editor
+                class="d-block w-75 description"
+                formControlName="description"
+                format="html"
+                [styles]="quillStyles"
+                [modules]="quillConfig"
+                [placeholder]="'A brief description of the organization'"
+              >
+              </quill-editor
+              >Organization description</label
             >
-            </quill-editor>
+
             <div *ngIf="editForm.get('description')?.invalid">
               <small
                 class="wide-text font-size-12 form-text text-danger"
@@ -326,22 +329,22 @@ <h2 class="wide-text font-size-24">Public details</h2>
                 'text-danger':
                   editForm.get('website')?.invalid && editForm.get('website')?.touched && editForm.get('website')?.dirty
               }"
-              >Website</label
+              ><input
+                type="text"
+                class="form-control"
+                name="website"
+                (input)="editForm.get('website')?.markAsUntouched()"
+                formControlName="website"
+                [ngClass]="{
+                  'text-danger':
+                    editForm.get('website')?.invalid &&
+                    editForm.get('website')?.touched &&
+                    editForm.get('website')?.dirty,
+                  'input-field-default-border': !editForm.get('website')?.touched || !editForm.get('website')?.dirty
+                }"
+              />Website</label
             >
-            <input
-              type="text"
-              class="form-control"
-              name="website"
-              (input)="editForm.get('website')?.markAsUntouched()"
-              formControlName="website"
-              [ngClass]="{
-                'text-danger':
-                  editForm.get('website')?.invalid &&
-                  editForm.get('website')?.touched &&
-                  editForm.get('website')?.dirty,
-                'input-field-default-border': !editForm.get('website')?.touched || !editForm.get('website')?.dirty
-              }"
-            />
+
             <ng-template #validWebsite>
               <small class="wide-text font-size-12 color-gray"
                 >Links should be in the full URL format e.g. http://www.website.com</small
@@ -365,19 +368,19 @@ <h2 class="wide-text font-size-24">Public details</h2>
             <label
               [ngClass]="{ 'text-danger': editForm.get('email')?.invalid && editForm.get('email')?.touched }"
               class="wide-text font-size-12"
-              >Email</label
+              ><input
+                type="text"
+                class="form-control"
+                name="email"
+                (input)="editForm.get('email')?.markAsUntouched()"
+                formControlName="email"
+                [ngClass]="{
+                  'text-danger': editForm.get('email')?.invalid && editForm.get('email')?.touched,
+                  'input-field-default-border': !editForm.get('email')?.touched
+                }"
+              />Email</label
             >
-            <input
-              type="text"
-              class="form-control"
-              name="email"
-              (input)="editForm.get('email')?.markAsUntouched()"
-              formControlName="email"
-              [ngClass]="{
-                'text-danger': editForm.get('email')?.invalid && editForm.get('email')?.touched,
-                'input-field-default-border': !editForm.get('email')?.touched
-              }"
-            />
+
             <ng-template #validEmail>
               <small class="wide-text font-size-12 color-gray"
                 >Emails should be in the standard format e.g. contactus&#64;website.com</small
@@ -420,7 +423,7 @@ <h4 class="w-33 font-size-14">Type</h4>
             <div class="w-66 line-height-150">{{ orgId.value }}</div>
             <div class="w-33 line-height-150">{{ orgId.key }}</div>
           </li>
-          <hr *ngIf="!(i == objectKeys(memberData.orgIds).length - 1)" class="mb-0 mt-0" />
+          <hr *ngIf="!(i === objectKeys(memberData.orgIds).length - 1)" class="mb-0 mt-0" />
         </ul>
       </div>
       <div *ngIf="memberData.isConsortiumLead">
diff --git a/ui/src/app/home/member-info/member-info-edit.component.ts b/ui/src/app/home/member-info/member-info-edit.component.ts
index 08f95af49..71ef9c27e 100644
--- a/ui/src/app/home/member-info/member-info-edit.component.ts
+++ b/ui/src/app/home/member-info/member-info-edit.component.ts
@@ -6,7 +6,7 @@ import { EMPTY, Subject, combineLatest } from 'rxjs'
 import { switchMap, take, takeUntil } from 'rxjs/operators'
 import { AccountService } from 'src/app/account'
 import { IAccount } from 'src/app/account/model/account.model'
-import { EMAIL_REGEXP, URL_REGEXP } from 'src/app/app.constants'
+import { EMAIL_REGEXP, URL_REGEXP, ORCID_BASE_URL } from 'src/app/app.constants'
 import { ISFAddress } from 'src/app/member/model/salesforce-address.model'
 import { ISFCountry } from 'src/app/member/model/salesforce-country.model'
 import { ISFState } from 'src/app/member/model/salesforce-country.model copy'
@@ -27,8 +27,8 @@ export class MemberInfoEditComponent implements OnInit, OnDestroy {
   memberData: ISFMemberData | undefined | null
   objectKeys = Object.keys
   orgIdsTransformed: KeyValue<string, string[]>[] = []
-  // TODO move to constants
-  MEMBER_LIST_URL: string = 'https://orcid.org/members'
+  ORCID_BASE_URL = ORCID_BASE_URL
+
   isSaving = false
   invalidForm: boolean | undefined
   managedMember: string | undefined
diff --git a/ui/src/app/user/users.component.ts b/ui/src/app/user/users.component.ts
index 41ba39a0f..1b747927c 100644
--- a/ui/src/app/user/users.component.ts
+++ b/ui/src/app/user/users.component.ts
@@ -1,9 +1,8 @@
 import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'
 import { IUser } from './model/user.model'
-import { Subscription, filter } from 'rxjs'
+import { Subscription } from 'rxjs'
 import { UserService } from './service/user.service'
-import { HttpErrorResponse } from '@angular/common/http'
-import { IUserPage, UserPage } from './model/user-page.model'
+import { IUserPage } from './model/user-page.model'
 import {
   faCheckCircle,
   faPencilAlt,

From fd94af1a87fe5ee0c7b6de525a7ba97b537beba5 Mon Sep 17 00:00:00 2001
From: andrej romanov <50377758+auumgn@users.noreply.github.com>
Date: Tue, 23 Apr 2024 23:59:29 +0300
Subject: [PATCH 3/3] stub test

---
 .../member-info-edit.component.spec.ts        | 28 ++++++++++---------
 1 file changed, 15 insertions(+), 13 deletions(-)

diff --git a/ui/src/app/home/member-info/member-info-edit.component.spec.ts b/ui/src/app/home/member-info/member-info-edit.component.spec.ts
index 447862b83..aa8bed363 100644
--- a/ui/src/app/home/member-info/member-info-edit.component.spec.ts
+++ b/ui/src/app/home/member-info/member-info-edit.component.spec.ts
@@ -1,21 +1,23 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing'
 
-import { MemberInfoEditComponent } from './member-info-edit.component';
+import { MemberInfoEditComponent } from './member-info-edit.component'
+import { AppModule } from 'src/app/app.module'
 
 describe('MemberInfoEditComponent', () => {
-  let component: MemberInfoEditComponent;
-  let fixture: ComponentFixture<MemberInfoEditComponent>;
+  let component: MemberInfoEditComponent
+  let fixture: ComponentFixture<MemberInfoEditComponent>
 
   beforeEach(() => {
     TestBed.configureTestingModule({
-      declarations: [MemberInfoEditComponent]
-    });
-    fixture = TestBed.createComponent(MemberInfoEditComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
+      imports: [AppModule],
+      declarations: [MemberInfoEditComponent],
+    })
+    fixture = TestBed.createComponent(MemberInfoEditComponent)
+    component = fixture.componentInstance
+    fixture.detectChanges()
+  })
 
   it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
+    expect(component).toBeTruthy()
+  })
+})