UNPKG

@360works/fmpromise

Version:

A modern JS toolkit for FileMaker Web Viewers, including a dev server and type generation.

78 lines 46.6 kB
��<?xml version="1.0"?> <FMAdd_on version="2.2.3.0" Source="22.0.3" File="fmPromise.fmp12" UUID="914A85CE-ED07-4417-90D2-1CF7E4EB8D2E" locale="English" Has_DDR_INFO="False" timestamp="2025-11-03T13:36:44"> <Data membercount="1"> <AddAction membercount="1"> <Records> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> <RowList membercount="1"> <Row membercount="12" id="1"> <Cell> <FieldReference type="Normal" datatype="Text" id="1" name="com.fmi.basetable.field.fmPromiseModule::4B68129F6621C41900B27BF59AB8FD9B" repetition="1" UUID="8B3BC1E7-A85F-49CC-96BA-2F3562FF22A6"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> <StyledText> <Data><![CDATA[C2FC32C2-5417-4641-8F67-EE66FD1798DF]]></Data> </StyledText> </Cell> <Cell> <FieldReference type="Normal" datatype="Timestamp" id="11" name="com.fmi.basetable.field.fmPromiseModule::4D23BC46444AF8A65D40FAD262C1ECE8" repetition="1" UUID="E511A9FE-8EEA-42AC-9D43-B12B6A1B59E1"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> <StyledText> <Data><![CDATA[10-24-2025 11:59:54 AM]]></Data> </StyledText> </Cell> <Cell> <FieldReference type="Normal" datatype="Text" id="7" name="com.fmi.basetable.field.fmPromiseModule::CAF925C0F6CA25D3A96F84D48240448A" repetition="1" UUID="E147AA02-B110-46F2-B4AA-D528038DF7FD"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> <StyledText> <Data><![CDATA[fm-promise-built-in/fm-promise-select-page.html]]></Data> </StyledText> </Cell> <Cell> <FieldReference type="Normal" datatype="Text" id="6" name="com.fmi.basetable.field.fmPromiseModule::406C3A21B8B1B9B99BAC8BE664F8CD44" repetition="1" UUID="6062AB51-E6C0-4613-99F8-697BFDD44DBD"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> <StyledText> <Data><![CDATA[ <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <style> html, body { height: 100%; margin: 0; padding: 1.5rem; font-family: -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif; color: #333; background-color: #f8f9fa; } a { color: #007bff; } h3, p { margin-top: 0; margin-bottom: 1rem; } strong { color: #000; } #success { color: #28a745; display: none; } #error, #fm-error-details { color: #dc3545; font-weight: bold; min-height: 1.2em; } .content-box { padding: 1.5rem; background: #fff; border: 1px solid #dee2e6; border-radius: 0.5rem; } #setup-instructions, #main-content, #success, #fm-error-instructions { display: none; } .form-control { display: flex; flex-direction: column; gap: 0.75rem; } input { width: 100%; padding: 0.5rem; border-radius: 0.25rem; border: 1px solid #ced4da; } button { padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; background-color: #007bff; color: white; cursor: pointer; } button:disabled { background-color: #6c757d; cursor: wait; } code { background-color: #e9ecef; padding: 0.2em 0.4em; border-radius: 3px; } ol { padding-left: 1.5rem; } #server-address-warning { color: #dc3545; font-weight: bold; display: none; border: 1px solid; padding: 0.5rem; border-radius: 0.25rem;} </style> <script type="module" crossorigin>(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))s(n);new MutationObserver(n=>{for(const o of n)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&s(i)}).observe(document,{childList:!0,subtree:!0});function r(n){const o={};return n.integrity&&(o.integrity=n.integrity),n.referrerPolicy&&(o.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?o.credentials="include":n.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(n){if(n.ep)return;n.ep=!0;const o=r(n);fetch(n.href,o)}})();class u extends Error{code;constructor({message:e="Unknown error",code:r}){super(e),this.name="FMPromiseError",this.code=r}toString(){return this.code?`${this.message} (${this.code})`:this.message}}let h=0;const c={},b=Promise.race([new Promise(t=>{if(window.FileMaker)t(window.FileMaker);else{let e;Object.defineProperty(window,"FileMaker",{get:()=>e,set:r=>t(e=r)})}}),new Promise((t,e)=>setTimeout(()=>e(new u({message:"FileMaker object not found within 5 seconds."})),5e3))]),l={get webViewerName(){return window.FMPROMISE_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer"},async performScript(t,e=null,r={}){const s=++h;console.log(`[fmPromise] #${s}: Calling script "${t}"`,e),e&&typeof e!="string"&&(e=JSON.stringify(e));const n=await b;let o=await new Promise((i,a)=>{c[s]={resolve:i,reject:a};const f=JSON.stringify({scriptName:t,promiseId:s,webViewerName:this.webViewerName,ignoreResult:r?.ignoreResult||void 0})+` `+(e||""),w=r.runningScript||0;w===0?n.PerformScript("fmPromise",f):n.PerformScriptWithOption("fmPromise",f,w.toString())});if(!r.alwaysReturnString&&typeof o=="string"&&(o.startsWith("{")||o.startsWith("[")))try{o=JSON.parse(o)}catch(i){console.warn(`[fmPromise] #${s}: Unable to parse JSON result.`,{result:o,error:i})}return console.log(`[fmPromise] #${s}: Received result.`,o),o},evaluate(t,e={},r={}){const n=`Let([${Object.entries(e||{}).map(([o,i])=>`${o}=${JSON.stringify(i)}`).join(";")}] ; ${t})`;return this.performScript("fmPromise.evaluate",n,r)},async executeFileMakerDataAPI(t){const e=await this.performScript("fmPromise.executeFileMakerDataAPI",t);if(!e||!e.messages||!e.messages.length)throw new u({code:-1,message:"Empty data API response"});if(e.messages[0].code!=="0")throw new u(e.messages[0]);return e},_processPortalRow(t,e){const r={},s=`${e}::`;for(const n in t)if(!(n==="recordId"||n==="modId"))if(n.startsWith(s)){const o=n.substring(s.length);r[o]=t[n]}else r[n]=t[n];return Object.defineProperties(r,{recordId:{value:t.recordId,enumerable:!1,writable:!0,configurable:!0},modId:{value:t.modId,enumerable:!1,writable:!0,configurable:!0}}),r},async executeFileMakerDataAPIRecords(t){const e=await this.executeFileMakerDataAPI(t);if(!e.response.data){const s=[];return Object.defineProperties(s,{foundCount:{value:0,enumerable:!1},totalRecordCount:{value:0,enumerable:!1}}),s}const r=e.response.data.map(s=>{const n=s.portalData||{},o={};for(const a in n){const m=n[a];o[a]=m.map(f=>this._processPortalRow(f,a))}const i={...s.fieldData,...o};return Object.defineProperties(i,{recordId:{value:s.recordId,enumerable:!1,writable:!0,configurable:!0},modId:{value:s.modId,enumerable:!1,writable:!0,configurable:!0}}),i});return Object.defineProperties(r,{foundCount:{value:e.response.dataInfo.foundCount,enumerable:!1},totalRecordCount:{value:e.response.dataInfo.totalRecordCount,enumerable:!1}}),r},async executeSql(t,...e){let r,s;if(Array.isArray(t)&&Array.isArray(t.raw)){if(e.length!==t.length-1)throw new u({code:-1,message:"Invalid template literal for executeSql"});r=t.join("?").replace(/\n\s*/g," "),s=e}else if(typeof t=="string")r=t,s=e;else throw new u({code:-1,message:"Invalid arguments: executeSql must be called with a SQL string, or as a template literal."});const n=s.map(m=>` ; ${JSON.stringify(m)}`).join(""),o=`|${Math.random()}|`,i=`~${Math.random()}~`,a=await this.evaluate(`ExecuteSQLe(${JSON.stringify(r)} ; "${o}" ; "${i}"${n})`,void 0,{alwaysReturnString:!0});return a===""||a===null||a===void 0?[]:a.split(i).map(m=>m.split(o))},insertFromUrl(t,e=""){return this.performScript("fmPromise.insertFromURL",{url:t,curlOptions:e})},setFieldByName(t,e){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:t,value:e})},async showCustomDialog(t,e,r="OK",s="",n=""){const o=await this.performScript("fmPromise.showCustomDialog",{title:t,body:e,btn1:r,btn2:s,btn3:n});return parseInt(o,10)||0},_resolve(t,e){c[t]&&(c[t].resolve(e),delete c[t])},_reject(t,e){if(c[t]){let r;try{r=JSON.parse(e)}catch{r={message:e}}console.warn(`[fmPromise] #${t}: Rejected.`,r),c[t].reject(new u(r)),delete c[t]}}};globalThis.fmPromise=l;globalThis.fmPromise_Resolve=l._resolve;globalThis.fmPromise_Reject=l._reject;const P=document.getElementById("main-content"),I=document.getElementById("setup-instructions"),v=document.getElementById("fm-error-instructions"),g=document.getElementById("myForm"),E=document.getElementById("success"),y=document.getElementById("error"),S=document.getElementById("existing-files"),x=document.getElementById("filename"),d=g.querySelector("button");function p(t){[P,I,v].forEach(e=>e.style.display="none"),document.getElementById(t).style.display="block"}async function F(t){t.preventDefault();const e=d.textContent;d.disabled=!0,d.textContent="Working...",y.textContent="";try{const r=x.value.trim().toLowerCase();await l.performScript("fmPromise.createOrRefreshModule",{filename:r,webViewerName:l.webViewerName}),g.style.display="none",E.style.display="block"}catch(r){y.textContent="Error: "+(r.message||r),d.disabled=!1,d.textContent=e}}async function M(){let t=[],e="";try{[t,e]=await Promise.all([l.executeSql`select filename from fmPromiseModule where includeInNewFiles=0 and filename not like 'fm-promise%'`,l.evaluate("_fmPromiseServerAddress")]),e=new URL(e+"/ping").toString()}catch(r){console.error("FileMaker bridge error:",r),p("fm-error-instructions");return}if(!e){document.getElementById("server-address-warning").style.display="block",p("setup-instructions");return}try{if(!(await l.insertFromUrl(e)).success)throw new Error("Invalid ping response");S.append(...t.map(s=>{let n=document.createElement("option");return n.value=s[0],n})),p("main-content"),g.addEventListener("submit",F)}catch(r){console.error("Dev server connection error:",r),p("setup-instructions")}}M();</script> </head> <body> <h3>360Works fmPromise Add-On</h3> <!-- View 1: Shown for FileMaker Bridge errors --> <div id="fm-error-instructions" class="content-box"> <h4>FileMaker Configuration Error</h4> <p>The web viewer cannot communicate with FileMaker. Please check the following:</p> <ol> <li>The web viewer object is configured to <strong>"Allow JavaScript to Perform FileMaker Scripts"</strong>.</li> <li>The web viewer object name is <code>fmPromiseWebViewer</code>.</li> <li>The fmPromise scripts and custom functions were installed correctly.</li> </ol> <div id="fm-error-details">Could not execute a FileMaker script from JavaScript.</div> </div> <!-- View 2: Shown if dev server is offline --> <div id="setup-instructions" class="content-box"> <h4>Welcome to fmPromise!</h4> <p>To create new web viewer modules, you need to run the local development server in a directory of your choice. fmPromise will create editable source code for your web viewer modules in this directory.</p> <p>Please follow these steps in a terminal in the directory you would like to use for web viewer content (this can be any directory on your machine):</p> <ol> <li> <strong>Install the package (you may need to <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm" target="_blank">Install NPM</a> first)</strong> <code>npm install @360works/fmpromise</code> </li> <li> <strong>Start the development server:</strong> <code>npx fmpromise-dev</code> </li> <li>Once the server is running, <strong>Reload this Web Viewer</strong> and call the <strong>fmPromise.toggleDevMode</strong> script to enable devMode.</li> </ol> <p>Consult the <a href="https://github.com/shmert/fmPromise" target="_blank">fmPromise documentation</a> for details.</p> <div id="server-address-warning"> <strong>Warning:</strong> The FileMaker global variable <code>$$fmPromise_Server_Address</code> is not set or is invalid. Please ensure the "fmPromise.OnFirstWindowOpen" script has run correctly. </div> <div id="error">Could not connect to the local development server.</div> </div> <!-- View 3: Shown on success --> <div id="main-content" class="content-box"> <p>You have successfully added a fmPromise web viewer to your layout! =� �</p> <p>Enter a <strong>unique path and/or filename</strong> for a new module:</p> <form id="myForm" role="form"> <div class="form-control"> <label for="filename">New Module Path (e.g., "invoices/main.html")</label> <input id="filename" name="filename" list="existing-files" placeholder="my-first-module" required/> <datalist id="existing-files"></datalist> <button type="submit">Create Module</button> </div> </form> </div> <!-- View 4: Shown after successful module creation --> <div id="success" class="content-box"> <h3>Your fmPromise module has been created!</h3> <p>The server has created the new files in your project's <code>src</code> directory.</p> <p>You may now close this setup panel and reload the web viewer to display the new module.</p> </div> </body> </html>]]></Data> </StyledText> </Cell> <Cell> <FieldReference type="Normal" datatype="Number" id="8" name="com.fmi.basetable.field.fmPromiseModule::6304F8E1826B38DDEB7C95B45D06B8D1" repetition="1" UUID="5C64C0D2-5730-4CC6-BC65-E28057C94A3D"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> <StyledText> <Data><![CDATA[0]]></Data> </StyledText> </Cell> <Cell> <FieldReference type="Normal" datatype="Text" id="9" name="com.fmi.basetable.field.fmPromiseModule::4561002D22704A42D003B57FA0E75A33" repetition="1" UUID="981F21E8-1A8D-49FE-8EBC-31558E72C7FA"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> <StyledText> <Data><![CDATA[When a new fmPromise add-on is dragged to a layout, this is the page which lets you configure which module to show in the web viewer.]]></Data> </StyledText> </Cell> <Cell> <FieldReference type="Normal" datatype="Text" id="10" name="com.fmi.basetable.field.fmPromiseModule::91F127C4B704FC5AC212BA970EB65F39" repetition="1" UUID="A136C2A8-707A-4481-96A4-4775725BEC80"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> <StyledText> <Data><![CDATA[<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <script type="module"> import fmPromise from "@360works/fmpromise"; // --- DOM Element References --- const mainContent = document.getElementById('main-content'); const setupInstructions = document.getElementById('setup-instructions'); const fmErrorInstructions = document.getElementById('fm-error-instructions'); const myForm = document.getElementById('myForm'); const successDiv = document.getElementById('success'); const errorDiv = document.getElementById('error'); const datalist = document.getElementById('existing-files'); const filenameInput = document.getElementById('filename'); const submitButton = myForm.querySelector('button'); /** Helper to show a specific view and hide others */ function showView(viewId) { [mainContent, setupInstructions, fmErrorInstructions].forEach(el => el.style.display = 'none'); document.getElementById(viewId).style.display = 'block'; } /** Handles the form submission to create a new module. */ async function handleFormSubmit(event) { event.preventDefault(); const originalButtonText = submitButton.textContent; submitButton.disabled = true; submitButton.textContent = 'Working...'; errorDiv.textContent = ''; try { const filename = filenameInput.value.trim().toLowerCase(); await fmPromise.performScript('fmPromise.createOrRefreshModule', { filename, webViewerName: fmPromise.webViewerName }); myForm.style.display = 'none'; successDiv.style.display = 'block'; } catch (e) { errorDiv.textContent = 'Error: ' + (e.message || e); submitButton.disabled = false; submitButton.textContent = originalButtonText; } } /** Main function to initialize the page and check connections. */ async function main() { let existingModules = []; let pingAddress = ''; // --- Stage 1: Check FileMaker Bridge --- try { [existingModules, pingAddress] = await Promise.all([ fmPromise.executeSql`select filename from fmPromiseModule where includeInNewFiles=0 and filename not like 'fm-promise%'`, fmPromise.evaluate('_fmPromiseServerAddress') ]); pingAddress = new URL(pingAddress + '/ping').toString(); // validate the address } catch (e) { console.error("FileMaker bridge error:", e); showView('fm-error-instructions'); return; // Stop execution } // --- Stage 2: Check Dev Server Connection --- if (!pingAddress) { // Show a specific part of the setup instructions if the variable isn't set document.getElementById('server-address-warning').style.display = 'block'; showView('setup-instructions'); return; } try { const pingResult = await fmPromise.insertFromUrl(pingAddress); if (!pingResult.success) throw new Error('Invalid ping response'); // --- Success State: Everything is working --- datalist.append(...existingModules.map(f => { let option = document.createElement('option'); option.value = f[0]; return option; })); showView('main-content'); myForm.addEventListener('submit', handleFormSubmit); } catch (e) { console.error("Dev server connection error:", e); showView('setup-instructions'); } } main(); </script> <style> html, body { height: 100%; margin: 0; padding: 1.5rem; font-family: -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif; color: #333; background-color: #f8f9fa; } a { color: #007bff; } h3, p { margin-top: 0; margin-bottom: 1rem; } strong { color: #000; } #success { color: #28a745; display: none; } #error, #fm-error-details { color: #dc3545; font-weight: bold; min-height: 1.2em; } .content-box { padding: 1.5rem; background: #fff; border: 1px solid #dee2e6; border-radius: 0.5rem; } #setup-instructions, #main-content, #success, #fm-error-instructions { display: none; } .form-control { display: flex; flex-direction: column; gap: 0.75rem; } input { width: 100%; padding: 0.5rem; border-radius: 0.25rem; border: 1px solid #ced4da; } button { padding: 0.5rem 1rem; border: none; border-radius: 0.25rem; background-color: #007bff; color: white; cursor: pointer; } button:disabled { background-color: #6c757d; cursor: wait; } code { background-color: #e9ecef; padding: 0.2em 0.4em; border-radius: 3px; } ol { padding-left: 1.5rem; } #server-address-warning { color: #dc3545; font-weight: bold; display: none; border: 1px solid; padding: 0.5rem; border-radius: 0.25rem;} </style> </head> <body> <h3>360Works fmPromise Add-On</h3> <!-- View 1: Shown for FileMaker Bridge errors --> <div id="fm-error-instructions" class="content-box"> <h4>FileMaker Configuration Error</h4> <p>The web viewer cannot communicate with FileMaker. Please check the following:</p> <ol> <li>The web viewer object is configured to <strong>"Allow JavaScript to Perform FileMaker Scripts"</strong>.</li> <li>The web viewer object name is <code>fmPromiseWebViewer</code>.</li> <li>The fmPromise scripts and custom functions were installed correctly.</li> </ol> <div id="fm-error-details">Could not execute a FileMaker script from JavaScript.</div> </div> <!-- View 2: Shown if dev server is offline --> <div id="setup-instructions" class="content-box"> <div id="error">Could not connect to the local development server.</div> <h4>Welcome to fmPromise!</h4> <p>To create new web viewer modules, you need to run the local development server in a directory of your choice. fmPromise will create editable source code for your web viewer modules in this directory.</p> <p>Please follow these steps in a terminal in the directory you would like to use for web viewer content (this can be any directory on your machine):</p> <ol> <li> <strong>Install the package (you may need to <a href="https://docs.npmjs.com/downloading-and-installing-node-js-and-npm" target="_blank">Install NPM</a> first)</strong> <code>npm install @360works/fmpromise</code> </li> <li> <strong>Start the development server:</strong> <code>npx fmpromise-dev</code> </li> <li>Once the server is running, <strong>Reload this Web Viewer</strong> and call the <strong>fmPromise.toggleDevMode</strong> script to enable devMode.</li> </ol> <p>Consult the <a href="https://github.com/shmert/fmPromise" target="_blank">fmPromise documentation</a> for details.</p> <div id="server-address-warning"> <strong>Warning:</strong> The FileMaker global variable <code>$$fmPromise_Server_Address</code> is not set or is invalid. Please ensure the "fmPromise.OnFirstWindowOpen" script has run correctly. </div> </div> <!-- View 3: Shown on success --> <div id="main-content" class="content-box"> <p>You have successfully added a fmPromise web viewer to your layout! =� �</p> <p>Enter a <strong>unique path and/or filename</strong> for a new module:</p> <form id="myForm" role="form"> <div class="form-control"> <label for="filename">New Module Path (e.g., "invoices/main.html")</label> <input id="filename" name="filename" list="existing-files" placeholder="my-first-module" required/> <datalist id="existing-files"></datalist> <button type="submit">Create Module</button> </div> </form> </div> <!-- View 4: Shown after successful module creation --> <div id="success" class="content-box"> <h3>Your fmPromise module has been created!</h3> <p>The server has created the new files in your project's <code>src</code> directory.</p> <p>You may now close this setup panel and reload the web viewer to display the new module.</p> </div> </body> </html> ]]></Data> </StyledText> </Cell> <Cell> <FieldReference type="Normal" datatype="Number" id="12" name="com.fmi.basetable.field.fmPromiseModule::73F9673850098681390CA23C51976B33" repetition="1" UUID="C632E5DC-43D3-48D6-93C0-543063A3B2E3"> <BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference> </FieldReference> </Cell> </Row> </RowList> </Records> </AddAction> </Data> </FMAdd_on>