diff --git a/app.js b/app.js index 90a736c..1d4a951 100644 --- a/app.js +++ b/app.js @@ -25,6 +25,7 @@ class App { patientEverythingToggleContainer: document.getElementById( "patient-everything-toggle-container" ), + editButton: document.getElementById("edit-button"), }; } @@ -42,6 +43,9 @@ class App { // Setup event listeners for the UI elements based on the capability statement this.setupEventListeners(capabilityStatement); } + + // Build the query string when the page loads + this.buildQueryString(); } // Asynchronous function to fetch and validate the capability statement @@ -112,22 +116,55 @@ class App { // Set up the event listeners for the pagination buttons this.setupPaginationButtons(); + + // Add an event listener to the "Edit" button that opens the edit page when clicked + this.getElement.editButton.addEventListener("click", () => + this.openEditPage(this.currentResource) + ); + + // Add an event listener to the read operation toggle + this.getElement.readOperationToggle.addEventListener("change", () => { + this.buildQueryString(); + }); + + // Add an event listener to the patient everything operation toggle + this.getElement.patientEverythingToggle.addEventListener("change", () => { + this.buildQueryString(); + }); + } + + openEditPage(resource) { + // Get the JSON representation of the FHIR resource + const resourceJson = JSON.stringify(resource, null, 2); + + // Encode the JSON string so it can be included in a URL + const encodedJson = encodeURIComponent(resourceJson); + + // Get the server URL + const serverUrl = this.getElement.serverUrl.value; + + // Encode the server URL so it can be included in a URL + const encodedServerUrl = encodeURIComponent(serverUrl); + + // Open the edit page in a new tab and pass the JSON and server URL as URL parameters + window.open( + `./pages/editor.html?resource=${encodedJson}&serverUrl=${encodedServerUrl}`, + "_blank" + ); } // Function to set up event listeners for pagination buttons setupPaginationButtons() { // Add an event listener to the "Next Page" button // When the button is clicked, it retrieves the next page of results - this.getElement.nextPageButton.addEventListener( - "click", - () => this.getNextPage() + this.getElement.nextPageButton.addEventListener("click", () => + this.getNextPage() ); // Add an event listener to the "Previous Page" button // When the button is clicked, it retrieves the previous page of results - this.getElement.previousPageButton.addEventListener( - "click", - () => this.getPreviousPage() + this.getElement.previousPageButton.addEventListener("click", () => + this.getPreviousPage() ); } @@ -156,6 +193,9 @@ class App { // Populate the parameters for the selected resource based on the capability statement this.populateParameters(selectedResource, capabilityStatement); + + // Build the query string when the resource type is changed + this.buildQueryString(); } // Function to handle the event when the form is submitted @@ -238,25 +278,30 @@ class App { // Method to handle addition of a new parameter by the user addParameter() { - // Retrieve user input for parameter name and value const parameterName = this.getElement.parameterNameInput.value; const parameterValue = this.getElement.parameterValueInput.value; - // If both parameter name and value are not empty if (parameterName && parameterValue) { - // Create a container element to display the user added parameter - const parameterContainer = this.createParameterContainer( - parameterName, - parameterValue - ); - - // Append this new parameter container to the parameters container element - this.getElement.parametersContainer.appendChild(parameterContainer); + let parameterContainer; + if (this.getElement.addParameterButton.dataset.editing) { + const parameterContainerId = + this.getElement.addParameterButton.dataset.editing; + parameterContainer = document.getElementById(parameterContainerId); + parameterContainer.querySelector(".userParameterName").innerText = + parameterName; + parameterContainer.querySelector(".userParameterValue").innerText = + parameterValue; + this.getElement.addParameterButton.innerText = "Add Parameter"; + delete this.getElement.addParameterButton.dataset.editing; + } else { + parameterContainer = this.createParameterContainer( + parameterName, + parameterValue + ); + this.getElement.parametersContainer.appendChild(parameterContainer); + } - // Rebuild the query string to include this new parameter this.buildQueryString(); - - // Clear the parameter input fields for next entry this.getElement.parameterNameInput.value = ""; this.getElement.parameterValueInput.value = ""; } @@ -320,11 +365,14 @@ class App { const searchParams = collectSearchParams(); // Create an instance of URLSearchParams with the collected search parameters - const queryParams = new URLSearchParams(searchParams); + const queryParams = new URLSearchParams(); + for (const [key, value] of searchParams) { + queryParams.append(key, value); + } // Check if the read operation toggle is checked const isReadOperation = this.getElement.readOperationToggle.checked; - + // Check if the patient everything operation is checked and the selected resource is 'Patient' const isPatientEverything = this.getElement.patientEverythingToggle.checked && @@ -333,33 +381,33 @@ class App { // Start building the URL with the server URL and the selected resource let queryString = `${serverUrl}/${resourceType}`; - // If it's a read operation and there's an _id parameter, append it to the URL - if (isReadOperation && searchParams._id) { - // Read operation - queryString += `/${searchParams._id}`; - } else if (isPatientEverything) { - // If it's a patient everything operation, append '$everything' to the URL + // Find the _id parameter in the searchParams array + const idParam = searchParams.find((param) => param[0] === "_id"); + + // If it's a patient everything operation, append '$everything' to the URL + if (isPatientEverything) { // If there's an _id parameter, append it to the URL before '$everything' - if (searchParams._id) { + if (idParam) { // Patient $everything operation with _id - queryString += `/${searchParams._id}/$everything`; + queryString += `/${idParam[1]}/$everything`; + // Remove the _id parameter from the queryParams queryParams.delete("_id"); } else { // Patient $everything operation without _id queryString += `/$everything`; } - - //TODO - I think this IF/ELSE statement is doing the same thing - // If there are any parameters left after deleting _id, append them to the URL - if (queryParams.toString().length > 0) { - queryString += `?${queryParams}`; - } - } else { - // If there are any parameters, append them to the URL - const hasSearchParams = Array.from(queryParams).length > 0; - queryString += `${hasSearchParams ? "?" : ""}${queryParams}`; + } else if (isReadOperation && idParam) { + // If it's a read operation and there's an _id parameter, append it to the URL + // Read operation + queryString += `/${idParam[1]}`; + // Remove the _id parameter from the queryParams + queryParams.delete("_id"); } + // If there are any parameters, append them to the URL + const hasSearchParams = Array.from(queryParams).length > 0; + queryString += `${hasSearchParams ? "?" : ""}${queryParams}`; + // Update the query string output text in the form if (this.getElement.queryStringOutput) { this.getElement.queryStringOutput.innerText = queryString; @@ -460,6 +508,8 @@ class App { const cardBody = createCardBody(resource); card.appendChild(cardBody); + + // Add an event listener to the card card.addEventListener("click", () => { const jsonString = JSON.stringify(resource, null, 2); @@ -481,14 +531,24 @@ class App { this.getElement.modal.style.display = "none"; } }; + + // Store the current resource + this.currentResource = resource; + + // Show the "Edit" button + this.getElement.editButton.style.display = "block"; }); return card; } + createParameterContainer(parameterName, parameterValue) { const parameterContainer = document.createElement("div"); parameterContainer.classList.add("form-group"); + // Assign an id to the parameterContainer + parameterContainer.id = `param-${parameterName}`; + const parameterNameElement = document.createElement("span"); parameterNameElement.classList.add("userParameterName"); parameterNameElement.label = "Parameter"; @@ -498,18 +558,38 @@ class App { parameterValueElement.classList.add("userParameterValue"); parameterValueElement.innerText = parameterValue; + const editButton = document.createElement("button"); + editButton.classList.add("edit-button"); + editButton.classList.add("btn"); + editButton.innerText = "Edit"; + editButton.addEventListener("click", (e) => { + e.preventDefault(); // Prevent the form from being submitted + this.getElement.parameterNameInput.value = parameterName; + this.getElement.parameterValueInput.value = parameterValue; + this.getElement.addParameterButton.innerText = "Update Parameter"; + this.getElement.addParameterButton.dataset.editing = parameterContainer; + this.getElement.addParameterButton.dataset.editing = + parameterContainer.id; + }); + const removeButton = document.createElement("button"); removeButton.classList.add("remove-button"); removeButton.classList.add("btn"); - removeButton.innerText = "x"; + removeButton.innerText = "Remove"; removeButton.addEventListener("click", () => { parameterContainer.remove(); this.buildQueryString(); }); + const buttonContainer = document.createElement("div"); + buttonContainer.classList.add("button-container"); + + buttonContainer.appendChild(editButton); + buttonContainer.appendChild(removeButton); + parameterContainer.appendChild(parameterNameElement); parameterContainer.appendChild(parameterValueElement); - parameterContainer.appendChild(removeButton); + parameterContainer.appendChild(buttonContainer); return parameterContainer; } @@ -596,7 +676,7 @@ function createTokenInput(parameter) { } function collectSearchParams() { - const searchParams = {}; + const searchParams = []; document .querySelectorAll( @@ -604,7 +684,7 @@ function collectSearchParams() { ) .forEach((input) => { if (input.value !== "") { - searchParams[input.name] = input.value; + searchParams.push([input.name, input.value]); } }); @@ -621,7 +701,7 @@ function collectSearchParams() { const parameterValue = parameterValueElement.innerText; if (parameterName && parameterValue) { - searchParams[parameterName] = parameterValue; + searchParams.push([parameterName, parameterValue]); } } }); diff --git a/index.html b/index.html index 84d5e69..68909b4 100644 --- a/index.html +++ b/index.html @@ -38,12 +38,12 @@
-
@@ -75,6 +75,7 @@

Query Result

diff --git a/pages/editor.html b/pages/editor.html new file mode 100644 index 0000000..1152036 --- /dev/null +++ b/pages/editor.html @@ -0,0 +1,42 @@ + + + + + Edit FHIR Resource + + + +
+

Edit FHIR Resource

+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+
+ +
+ + + + \ No newline at end of file diff --git a/pages/editor.js b/pages/editor.js new file mode 100644 index 0000000..4d1a7c2 --- /dev/null +++ b/pages/editor.js @@ -0,0 +1,80 @@ +window.addEventListener("load", () => { + // Get the JSON and server URL from the URL parameters + const urlParams = new URLSearchParams(window.location.search); + const resourceJson = urlParams.get("resource"); + const serverUrl = urlParams.get("serverUrl"); + + // Decode the JSON string and server URL + const decodedJson = decodeURIComponent(resourceJson); + const decodedServerUrl = decodeURIComponent(serverUrl); + + // Populate the text area with the JSON + const jsonEditor = document.getElementById("json-editor"); + jsonEditor.value = decodedJson; + + // Adjust the rows attribute of the textarea based on the number of lines in the JSON + jsonEditor.rows = decodedJson.split('\n').length; + + // Set the value of the server URL input field + document.getElementById("server-url").value = decodedServerUrl; + + // Add an event listener for the save button + document + .getElementById("save-button") + .addEventListener("click", () => saveChanges(decodedServerUrl)); +}); + +async function saveChanges() { + // Get the edited JSON from the text area + const editedJson = document.getElementById("json-editor").value; + + // Get the server URL from the input field + const serverUrl = document.getElementById("server-url").value; + + // Get the selected HTTP method + const httpMethod = document.getElementById("http-method").value; + + // Convert the JSON to a JavaScript object + const editedResource = JSON.parse(editedJson); + + // Construct the URL + let url = `${serverUrl}/${editedResource.resourceType}`; + if (httpMethod === "PUT") { + url += `/${editedResource.id}`; + } + + // Send the edited resource to the server + const response = await fetch(url, { + method: httpMethod, + headers: { + "Content-Type": "application/fhir+json", + }, + body: editedJson, + }); + + // Get the response text + const responseText = await response.text(); + + // Create a string with the status and headers + const responseInfo = `Status: ${response.status} ${ + response.statusText + }\n\nHeaders:\n${Array.from(response.headers.entries()) + .map(([key, value]) => `${key}: ${value}`) + .join("\n")}\n\nBody:\n${responseText}`; + + // Display the response in the modal + document.getElementById("json-display").textContent = responseInfo; + document.getElementById("json-modal").style.display = "block"; + + // Close the modal when the close button is clicked + document.getElementById("close-modal").onclick = () => { + document.getElementById("json-modal").style.display = "none"; + }; + + // Close the modal when clicked outside the modal content + window.onclick = (event) => { + if (event.target === document.getElementById("json-modal")) { + document.getElementById("json-modal").style.display = "none"; + } + }; +} diff --git a/resources/.DS_Store b/resources/.DS_Store new file mode 100644 index 0000000..294446e Binary files /dev/null and b/resources/.DS_Store differ diff --git a/resources/favicon.ico b/resources/favicon.ico deleted file mode 100644 index c7c0b26..0000000 Binary files a/resources/favicon.ico and /dev/null differ diff --git a/resources/old-styles.css b/resources/old-styles.css deleted file mode 100644 index 9ffb036..0000000 --- a/resources/old-styles.css +++ /dev/null @@ -1,307 +0,0 @@ -/* Add some basic styles for the page */ -body { - font-family: "Roboto", sans-serif; - margin: 0; - padding: 0; - /* background-color: #f5f5f5; */ - background-color: #7c3e3e; -} - -.ascii-art { - /* Typography */ - font-family: 'Courier New', Courier, monospace; /* Monospace font for uniform character width */ - font-size: 16px; - font-weight: bolder; - color: white; /* Dark gray color for the text */ - - /* Optional: Control width and overflow */ - max-width: 100%; /* Limit width to 90% of the parent container */ - overflow-x: auto; /* Add horizontal scrollbar if content overflows */ - text-align: center; -} - - -.container { - flex-direction: column; - align-items: center; - padding: 2rem; -} - -h1 { - margin: 0; - text-align: center; - color: #4a4a4a; -} - -form { - display: flex; - flex-direction: column; - margin-top: 2rem; -} - -.form-group { - display: flex; - flex-direction: column; - margin-bottom: 1rem; -} - -.form-operation-toggle { - display: block; -} - -.form-group label { - margin-bottom: 0.5rem; - color: #4a4a4a; -} - -.form-group input[type="text"], -.form-group input[type="date"], -.form-group select { - padding: 0.5rem; - font-size: 1rem; - border-bottom: 1px solid #ccc; - border-top: none; - border-left: none; - border-right: none; - background-color: #f9f9f9; -} - -.form-group input.invalid-server-url { - background-color: #bc6367; -} - -textarea:focus, -input:focus { - outline: none; - background-color: #f1f1f1; - opacity: 1; -} - -.form-group select { - width: 100%; -} - -.form-row { - display: flex; - justify-content: space-between; - align-items: center; - background-color: #fbfafa; - border: 1px solid #ccc; - border-radius: 0.5rem; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); - padding: 1rem; - margin-top: 2rem; -} - -.form-col { - flex: 1; - margin-right: 20px; -} - -.grid-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - grid-gap: 20px; - margin-top: 20px; -} - -#parameters-container, -.server-container { - background-color: #fbfafa; - border: 1px solid #ccc; - border-radius: 0.5rem; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); - padding: 1rem; - margin-top: 2rem; -} - -#query-button { - margin-top: 2rem; - background-color: #00a65a; -} - -.btn { - padding: 0.5rem 1rem; - background-color: #aaaaaa; - border: none; - border-radius: 0.5rem; - color: #fff; - font-size: 1rem; - cursor: pointer; -} - -#add-parameter-button { - bottom: 0; -} - -.btn:hover { - background-color: #367fa9; -} - -.result-container { - display: grid; - grid-template-columns: 1fr; - grid-gap: 1rem; -} - -.bundle-card-container { - display: grid; - grid-template-columns: 1fr; - margin-top: 1rem; -} - -.card-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - grid-gap: 1rem; -} - -.result-card { - background-color: #fbfafa; - color: #4a4a4a; - border: 1px solid #ccc; - border-radius: 0.5rem; - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); - padding: 1rem; - margin-top: 2rem; -} - -.result-card h2 { - margin-top: 0; - color: #4a4a4a; -} - -.card { - background-color: #fbfafa; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - grid-gap: 1rem; - border: 1px solid #888; - border-radius: 0.5rem; - padding: 1rem; - margin-bottom: 1rem; -} - -.modal { - display: none; - position: fixed; - z-index: 1; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0, 0, 0, 0.4); -} - -.modal-content { - background-color: #fefefe; - margin: 15% auto; - padding: 20px; - border: 1px solid #888; - border-radius: 0.5rem; - width: 80%; -} - -.json-display { - border-radius: 0.5rem; -} - -.close { - color: #aaaaaa; - float: right; - font-size: 28px; - font-weight: bold; -} - -.close:hover, -.close:focus { - color: #000; - text-decoration: none; - cursor: pointer; -} - -/* Tooltip container */ -label { - position: relative; - display: inline-block; - cursor: help; -} - -/* Tooltip text */ -.tooltip-text { - visibility: hidden; - width: 200px; - background-color: #555; - color: #fff; - text-align: center; - padding: 5px; - border-radius: 3px; - position: absolute; - z-index: 1; - bottom: 125%; /* Position the tooltip above the label */ - left: 50%; - margin-left: -100px; /* Center the tooltip */ - opacity: 0; - transition: opacity 0.3s; -} - -/* Show the tooltip when hovering over the label */ -label:hover .tooltip-text { - visibility: visible; - opacity: 1; -} - - -/* highlighjs copy feature */ -.hljs-copy-wrapper { - position: relative; - overflow: hidden; -} -.hljs-copy-wrapper:hover .hljs-copy-button, -.hljs-copy-button:focus { - transform: translateX(0); -} -.hljs-copy-button { - position: absolute; - transform: translateX(calc(100% + 1.125em)); - top: 1em; - right: 1em; - width: 2rem; - height: 2rem; - text-indent: -9999px; - color: #fff; - border-radius: 0.25rem; - border: 1px solid #ffffff22; - background-color: #2d2b57; - background-color: #4a4a4a; - background-image: url('data:image/svg+xml;utf-8,'); - background-repeat: no-repeat; - background-position: center; - transition: background-color 200ms ease, transform 200ms ease-out; -} -.hljs-copy-button:hover { - border-color: #ffffff44; -} -.hljs-copy-button:active { - border-color: #ffffff66; -} -.hljs-copy-button[data-copied="true"] { - text-indent: 0; - width: auto; - background-image: none; -} -@media (prefers-reduced-motion) { - .hljs-copy-button { - transition: none; - } -} -.hljs-copy-alert { - clip: rect(0 0 0 0); - clip-path: inset(50%); - height: 1px; - overflow: hidden; - position: absolute; - white-space: nowrap; - width: 1px; -} \ No newline at end of file diff --git a/resources/styles.css b/resources/styles.css index 336a917..a16c491 100644 --- a/resources/styles.css +++ b/resources/styles.css @@ -53,9 +53,9 @@ form { .form-group select { padding: 0.5rem; font-size: 1rem; - background-color: #a86969; /* Darker, earthy tone */ - border: 1px solid #f5e6d8; - color: #f5e6d8; + background-color: #f5e6d8; /* Darker, earthy tone #a86969 */ + border: 1px solid #a86969; + color: #a86969; } .form-group input.invalid-server-url { @@ -63,11 +63,12 @@ form { color: #f5e6d8; } -textarea:focus, -input:focus { +textarea, +input { outline: none; - background-color: #b97878; /* Slightly lighter tone for focus */ + background-color: #f5e6d8; /* Slightly lighter tone for focus */ opacity: 1; + resize: none; } .form-group select { @@ -77,14 +78,22 @@ input:focus { .form-row, #parameters-container, .server-container, -.result-card, -.card { +.result-card { background-color: #a86969; border: 2px solid #f5e6d8; /* Thicker border */ padding: 1rem; margin-top: 2rem; } +.card { + background-color: #f5e6d8; + color: #a86969; + padding: 1rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-gap: 1rem; +} + .grid-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); @@ -148,15 +157,9 @@ input:focus { color: #f5e6d8; } -.card { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - grid-gap: 1rem; -} - /* Standard syntax */ input::placeholder { - color: #f5e6d8; + color: #a86969; font-family: "Courier New", Courier, monospace; opacity: 0.7; /* Use opacity for lighter color; not all browsers support the 'color' property for placeholders */ } @@ -176,6 +179,22 @@ input::placeholder { background-color: #e5d6c8; } +.button-container { + display: flex; + justify-content: space-between; +} + +.edit-button, .remove-button { + flex: 1; + margin: 0.5em; +} + +#json-editor { + font-family: "Courier New", Courier, monospace; + white-space: pre; + overflow: auto; +} + .result-container { display: grid; grid-template-columns: 1fr;