<?xml version="1.0"?>
<FMAdd_on version="2.1.0.0" Source="19.3.2" File="fmPromise.fmp12" UUID="914A85CE-ED07-4417-90D2-1CF7E4EB8D2E" locale="English" timestamp="2021-09-12T18:57:25">
	<Data membercount="1">
		<AddAction membercount="1">
			<Records>
				<BaseTableReference id="129" name="com.fmi.basetable.CE5A8F37EDDA1C956ECC9074C2253BEB" UUID="73DD828A-6FC6-4B6B-8B6D-5ACAE6F523CA"></BaseTableReference>
				<RowList membercount="8">
					<Row membercount="11" 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[4DC20727-BE84-4B95-8094-8D92921212E1]]></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[09-12-2021 06:54:44 PM]]></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.js]]></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["use strict";const fmPromise=function(){let e=0;const t={},r={},n=new Promise((e,t)=>{let r=100;const n=window.setInterval(()=>{window.FileMaker?(window.clearInterval(n),e(window.FileMaker)):r--||(window.clearInterval(n),t("No window.FileMaker object was loaded after polling timeout."))},10)});function o(e){Object.assign(this,e),this.message=e.message||"Unknown error",this.name=this.constructor.name}return o.prototype=new Error,o.prototype.toString=function(){return this.message+(this.code?" ("+this.code+")":"")},{webViewerName:document.$FMP_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer",async configuration(e){let t=Object.entries(e).reduce((e,t)=>(e[t[0]]=t[1].value,e),{});await n;const r=await this.executeSql("select configuration from fmPromiseWebViewer where id=?",this.webViewerName);if(r.length&&r[0][0])try{const n=JSON.parse(r[0][0]);let o=Object.entries(e).filter(e=>{let[t,r]=e;return"string"==typeof r&&(r={type:r}),!1!==r.required&&"boolean"!==r.type&&!n[t]});if(0===o.length)return n;console.warn("Config is out of date",r,"missing key(s)",o),Object.assign(t,n)}catch(e){console.error("Could not parse",r,e)}await fmPromise.loadModule("fm-promise-config-form.js");const o=await fmPromiseConfiguration.show(e,t);return fmPromise.performScript("fmPromise.configure",o)},async loadModule(e){if(!r[e])return r[e]=!0,new Promise(async(t,r)=>{const n=document.createElement("script");if(n.type="text/javascript",0===e.indexOf("http"))n.src=src,n.onload=function(){t(src)};else{const s=await this.executeSql("select coalesce(sourceMinified, source) from fmPromiseModule where filename=?",e);s.length||r(new o({message:"Unable to locate fmPromise module "+e})),n.text=s[0][0],t(s)}document.head.appendChild(n)})},performScript(r,o=null,s={runningScript:0,alwaysReturnString:!1}){o&&"string"!=typeof o&&(o=JSON.stringify(o));const i=s.runningScript||0,a=++e;return n.then(e=>new Promise((n,s)=>{const c=this.webViewerName,l=JSON.stringify({scriptName:r,promiseId:a,webViewerName:c});t[a]={resolve:n,reject:s},console.info(a+' Performing script "'+r+'" with param '+(o?o.length:0)+" bytes");const m=l+"\n"+o;0===i?e.PerformScript("fmPromise",m):e.PerformScriptWithOption("fmPromise",m,i)})).then(e=>{if(!s.alwaysReturnString&&e&&"{"===e[0]||"["===e[0])try{e=JSON.parse(e)}catch(t){console.warn("Unable to parse JSON result ",e,t)}return e})},evaluate(e,t={},r={}){const n="Let(["+Object.entries(t||{}).map(e=>e[0]+"="+JSON.stringify(e[1])).join(";")+"] ; "+e+")";return this.performScript("fmPromise.evaluate",n,r)},executeFileMakerDataAPI(e){return this.performScript("fmPromise.executeFileMakerDataAPI",e).then(e=>{if(e&&e.messages&&e.messages.length){if("0"!==e.messages[0].code)throw new o(e.messages[0]);return e.response}throw new o({code:-1,message:"Empty data API response"})})},async executeFileMakerDataAPIRecords(e){return(await this.executeFileMakerDataAPI(e)).data.map(e=>{const t=e.fieldData;return t.recordId=e.recordId,t.modId=e.modId,Object.assign(t,e.portalData),t})},executeSql(e,...t){const r=t.map(e=>" ; "+JSON.stringify(e)).join(""),n="|"+Math.random()+"|",o="~"+Math.random()+"~";return this.evaluate("ExecuteSQL("+JSON.stringify(e)+' ; "'+n+'" ; "'+o+'"'+r+" )",null,{alwaysReturnString:!0}).then(e=>0===e.length?[]:e.split(o).map(e=>e.split(n)))},executeSqlTemplate(e,...t){if(!Array.isArray(e)||t.length!==e.length-1)throw new o({code:-1,message:"Invalid template literal for executeSqlTemplate"});return this.executeSql(e.join("?"),t)},insertFromUrl(e,t=""){return this.performScript("fmPromise.insertFromURL",{url:e,curlOptions:t})},setFieldByName(e,t){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:e,value:t})},showCustomDialog(e,t,r="OK",n="",o=""){return this.performScript("fmPromise.showCustomDialog",{title:e,body:t,btn1:r,btn2:n,btn3:o}).then(function(e){return parseInt(e)})},_resolve:(e,r)=>(console.info(e+" Resolve with "+r.length+" chars"),t[e].resolve(r),delete t[e]),_reject(e,r){let n;try{n=JSON.parse(r)}catch(e){console.warn("Unable to parse error response as JSON",r,e),n={message:r}}return console.warn(e+" Reject",r),t[e].reject(new o(n)),delete t[e]}}}();]]></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[1]]></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[JavaScript library which is included with newly created HTML files.]]></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['use strict';/** * Copyright 2020 360Works * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *     http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *//** * fmPromise helps you utilize web viewers in your solution with the minimum amount of fuss. */// noinspection DuplicatedCodeconst fmPromise = function () {	let lastPromiseId = 0;	const callbacksById = {};	const loadedModules = {};	const fmProxy = new Promise((resolve, reject) => {		let triesLeft = 100;		const pollingId = window.setInterval(() => {			// noinspection JSUnresolvedVariable,JSUnresolvedFunction			if (window.FileMaker) {				window.clearInterval(pollingId);				resolve(window.FileMaker);			} else if (!triesLeft--) {				window.clearInterval(pollingId);				reject('No window.FileMaker object was loaded after polling timeout.');			}		}, 10);	});	/**	 * Custom error type for scripting engine error results.	 * @constructor	 */	function FMPromiseError(obj) {		Object.assign(this, obj);		this.message = obj.message || 'Unknown error';		this.name = this.constructor.name;	}	FMPromiseError.prototype = new Error();	FMPromiseError.prototype.toString = function () {		// noinspection JSUnresolvedVariable		return this.message + (this.code ? ' (' + this.code + ')' : '');	}	return {		/**		 * The default <code>webViewerName</code> is "fmPromiseWebViewer". This corresponds to the webViewer layout object by FileMaker to call back into JavaScript.		 * @param s:string		 */		webViewerName: document.$FMP_WEB_VIEWER_NAME || new URLSearchParams(window.location.search).get('webViewerName') || 'fmPromiseWebViewer',		/**		 * Returns a configuration promise of type `schema` for the current webViewer instance.		 * If no configuration has been saved, or the configuration does not have all the required fields, a configuration dialog is displayed.		 * Upon submitting this configuration, the regular component is displayed again, and the configuration promise is resolved.		 * <h3>Schema Definition</h3>		 * The schema object defines the types of inputs to use for the required configuration attributes. Each element in `schema` must have a `type` of `"field"` or `"text"`.		 * <p>		 *     for `field` types, you may optionally specify a `tableFilter()` function which accepts `{id, name, baseTableName, fileName, modCount}` table objects and returns a boolean value.		 * <p>N		 *     for `field` types, you may optionally specify a `fieldFilter()` function which accepts `{id, name, tableName, type, class, reps, modCount}` column objects and returns a boolean value.		 *		 * @param schema defines the configuration options for a module. Keys are names, values are string types or schema objects {type, value, required, tableFilter, columnFilter, scriptFilter}		 * @return {Promise<object>} configuration payload for a single web viewer instance.		 */		async configuration(schema) {			// copy default values into result			let result = Object.entries(schema)				.reduce((prev, cur) => {					prev[cur[0]] = cur[1].value;					return prev;				}, {});			await fmProxy; // wait for the webViewerName to be set			// check the fmPromiseWebViewer table for a previously-saved configuration			// noinspection SqlResolve			const existingConfig = await this.executeSql('select configuration from fmPromiseWebViewer where id=?', this.webViewerName);			if ( existingConfig.length && existingConfig[0][0] ) {				try {					const oldConfig = JSON.parse(existingConfig[0][0]);					// Verify that the saved config has all the keys requested in the schema. If not, we want to show a config dialog to update the stale config					let missingKeys = Object.entries(schema).filter(entry => {						let [name,definition] = entry;						if (typeof definition === 'string') {							definition = {type: definition}; // shorthand						}						return definition.required !== false && definition.type !== 'boolean' && !oldConfig[name];					});					if (missingKeys.length === 0) {						return oldConfig;					}					console.warn('Config is out of date', existingConfig, 'missing key(s)', missingKeys);					Object.assign(result, oldConfig); // use old values if possible				} catch (e) {					console.error('Could not parse', existingConfig, e);				}			}			await fmPromise.loadModule('fm-promise-config-form.js')			const payload = await fmPromiseConfiguration.show(schema, result);			return fmPromise.performScript('fmPromise.configure', payload)		},		async loadModule(moduleName) {			if (loadedModules[moduleName]) {				return; // already loaded it			}			loadedModules[moduleName] = true; // pass or fail, we only want to do it once			return new Promise(async (resolve, reject) => {				const script = document.createElement('script');				script.type = 'text/javascript';				if (moduleName.indexOf('http') === 0) {					// load this script from elsewhere					script.src = src;					script.onload = function () {						resolve(src);					};				} else {					// load the script from an fmPromiseModule record					// noinspection SqlResolve					const src = await this.executeSql('select coalesce(sourceMinified, source) from fmPromiseModule where filename=?', moduleName);					if ( !src.length ) {						reject(new FMPromiseError({message:'Unable to locate fmPromise module ' + moduleName}));					}					script.text = src[0][0];					resolve(src);				}				document.head.appendChild(script); //or something of the likes			})		},		/**		 * Performs a FileMaker script, returning a Promise.		 * The promise will be rejected if the FileMaker script result starts with the word "ERROR".		 * <p>		 * The Promise will be resolved with parsed JSON if possible, unless `options.alwaysReturnString` is `true`.		 * <p>		 * You can specify `options.runningScript` to control how currently running FileMaker script is handled. 0:continue, 1:halt, 2:exit, 3:Resume, 4:Pause, 5:Interrupt.		 * See https://filemakersupport.force.com/en/s/article/FileMaker-Pro-19-1-2-Updater-Release-Notes		 * @return {Promise} which will be resolved / rejected by the `scriptName` being called		 * @param scriptName{string} FileMaker script to perform		 * @param scriptParameter{any} optional parameter to pass to the script		 */		performScript(scriptName, scriptParameter=null, options={runningScript:0, alwaysReturnString:false }) {			if (scriptParameter && typeof scriptParameter !== 'string') {				scriptParameter = JSON.stringify(scriptParameter);			}			const option = options.runningScript || 0;			const promiseId = ++lastPromiseId;			return fmProxy				.then((fm) => {					return new Promise((resolve, reject) => {						const webViewerName = this.webViewerName; // get this as late as possible, in case it has changed						const meta = JSON.stringify({scriptName, promiseId, webViewerName});						callbacksById[promiseId] = {resolve, reject};						console.info(promiseId + ' Performing script \"' + scriptName + '\" with param ' + (scriptParameter ? scriptParameter.length : 0) + ' bytes');						// noinspection JSUnresolvedFunction						const comboParam = meta + '\n' + scriptParameter;						if (option === 0) { // use PerformScript for backwards compatibility prior to FileMaker 9.1.2							// noinspection JSUnresolvedFunction							fm.PerformScript('fmPromise', comboParam);						} else {							// noinspection JSUnresolvedFunction							fm.PerformScriptWithOption('fmPromise', comboParam, option);						}					})				})				.then((result) => { // try parsing FM result if it looks like JSON					if (!options.alwaysReturnString && result && result[0] === '{' || result[0] === '[') {						try {							// noinspection JSCheckFunctionSignatures							result = JSON.parse(result);						} catch (e) {							console.warn('Unable to parse JSON result ', result, e);						}					}					return result;				});		},		/**		 * Evaluate an expression in FileMaker using optional letVars. </p>		 * <p>This is also a handy way to set $$GLOBAL variables, since any values in the `letVars` object are set in a `Let(...)` statement. e.g.:		 * ```		 * fmPromise.evaluate(a+b, {a:1, b:2, $$LAST_ACTION:'Add'})		 * ```		 * @param exp:string calculation to evaluate		 * @param letVars:object key/value pairs which may be used in <code>exp</code>		 * @param options {}		 * @return {Promise} containing the evaluated result		 */		evaluate(exp, letVars = {}, options={}) {			const letEx = Object.entries(letVars || {}).map((o) => o[0] + '=' + JSON.stringify(o[1])).join(';');			const stmt = 'Let([' + letEx + '] ; ' + exp + ')';			return this.performScript('fmPromise.evaluate', stmt, options);		},		/**		 * Execute a FileMaker Data API call with the given parameter.		 * <p>		 * If you are simply fetching record data, you may want to use #executeFileMakeDataAPIRecords which returns an array of just the fieldData objects without any of the other metadata.		 *		 * For example, to perform a multi-part find request on a layout, returning the first 10 rows:		 * await fmPromise.executeFileMakerDataAPI(		 * {		 *     layouts:'Contacts',		 *     limit: 10,		 *     query: [		 *         {City: 'San Francisco'},		 *         {City: 'Atlanta'}		 *     ],		 *     sort: [		 *         {fieldName:'lastName', sortOrder:'ascend'},		 *         {fieldName:'firstName', sortOrder:'ascend'}		 *     ]		 * }		 * )		 * @param {{layouts: string, layout.response:undefined|string, action: 'metaData'|'read'|undefined, limit: number|undefined, offset:undefined|number, query:undefined|{omit:undefined|'true'}[], sort:undefined|{fieldName:string,sortOrder:'ascend'|'descend'|undefined}[], portal:undefined|string[]}} param the data API call parameter JSON		 * @return {Promise<{layouts:undefined|[{name:string,table:string}], fieldMetaData:undefined|[{}], portalMetaData:undefined|[{}], valueLists:undefined|[{}] dataInfo:undefined|{database:string, layout:string, table:string, totalRecordCount:number, foundCount:number, returnedCount:number}, data:undefined|[{fieldData:{}, portalData:{}, portalDataInfo:[{}]}]}>}		 */		executeFileMakerDataAPI(param) {			return this.performScript('fmPromise.executeFileMakerDataAPI', param)				.then((result) => {					// do error-checking on the data API result here instead of in FileMaker, as JSON parsing for large payloads is faster					if (!result || !result.messages || !result.messages.length) {						throw new FMPromiseError({code:-1,message:'Empty data API response'});					} else if (result.messages[0].code !== '0') {						throw new FMPromiseError(result.messages[0]); // has a code and message, conveniently					} else {						return result.response;					}				});		},		/**		 * This is syntactic sugar for the {@link #executeFileMakerDataAPI} when all you want is the `fieldData` from the `data` array, which is most of the time.		 * @param {{layouts: string, layout.response:undefined|string, action: 'read'|undefined, limit: number|undefined, offset:undefined|number, query:undefined|{omit:undefined|'true'}[], sort:undefined|{fieldName:string,sortOrder:'ascend'|'descend'|undefined}[], portal:undefined|string[]}} param the data API call parameter JSON		 * @return {Promise<[{}]>}		 */		async executeFileMakerDataAPIRecords(param) {			return (await this.executeFileMakerDataAPI(param))				.data				.map( o => {					const rec = o.fieldData;					rec.recordId = o.recordId;					rec.modId = o.modId;					Object.assign(rec, o.portalData); // copy portal data onto the record					return rec				} );		},		/**		 * Executes a SQL command with placeholders, parsing the plain-text delimited result into an array of arrays.		 * @param sql:string		 * @param bindings:{}		 * @return {Promise<Array<Array<String>>>}		 */		executeSql(sql, ...bindings) {			const p = bindings.map((o) => ' ; ' + JSON.stringify(o)).join('');			const colDelim = '|' + Math.random() + '|';			const rowDelim = '~' + Math.random() + '~';			return this.evaluate('ExecuteSQL(' + JSON.stringify(sql) + ' ; "' + colDelim + '" ; "' + rowDelim + '"' + p + ' )', null, {alwaysReturnString: true})				.then((rawData) => {					return rawData.length === 0 ? [] : rawData.split(rowDelim).map((r) => r.split(colDelim));				});		},		/**		 * Convenience method for <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals">template-literal</a>		 * SQL queries with inline-style parameters, but still escaping user input. <br>		 * For example, to find Bobby Tables:		 * <pre>		 *     let q = ';drop table students';		 *     let resultSet = fmPromise.executeSqlTemplate`select * from students where name=${q}`		 * </pre>		 */		executeSqlTemplate(strings, ...args) {			if ( !Array.isArray(strings) || args.length !== strings.length-1) {				throw new FMPromiseError({code:-1,message:'Invalid template literal for executeSqlTemplate'})			}			return this.executeSql(strings.join('?'), args);		},		/**		 * Inserts from URL without worrying about cross-site scripting limitations imposed on the web viewer		 * @param url:string URL to fetch/post to		 * @param curlOptions:string @see https://fmhelp.filemaker.com/help/18/fmp/en/index.html#page/FMP_Help/curl-options.html		 * @return {Promise<string>} the response body. If you need headers, consider writing a custom FileMaker script.		 */		insertFromUrl(url, curlOptions='') {			return this.performScript('fmPromise.insertFromURL', {url, curlOptions});		},		/**		 * Sets a field by name in FileMaker.		 * @param fmFieldNameToSet:string The name of the field, optionally fully qualified		 * @param value		 * @return {Promise}		 */		setFieldByName(fmFieldNameToSet, value) {			return this.performScript('fmPromise.setFieldByName', {fmFieldNameToSet, value});		},		/**		 * Shows a dialog in FileMaker, and returns the (one-based!) index of the button chosen		 * @param title:string		 * @param body:string		 * @param btn1:string		 * @param btn2:string		 * @param btn3:string		 * @return {Promise<number>}		 */		showCustomDialog(title, body, btn1='OK', btn2='', btn3='') {			return this.performScript('fmPromise.showCustomDialog', {title, body, btn1, btn2, btn3})				.then(function(chosenMessage) {					// noinspection JSCheckFunctionSignatures					return parseInt(chosenMessage);				});		},		/**		 * Private method called by FileMaker to provide a result for a script call.		 * @return {boolean} whether there was a pending promise with this id		 */		_resolve(promiseId, result) {			console.info(promiseId + ' Resolve with ' + result.length + ' chars');			callbacksById[promiseId].resolve(result);			return delete callbacksById[promiseId];		},		/**		 * Private method called by FileMaker to provide an error cause for a script call.		 * @return {boolean} whether there was a pending promise with this id		 */		_reject(promiseId, errorString) {			let errorObj;			try {				errorObj = JSON.parse(errorString);			} catch ( parseError ) {				console.warn('Unable to parse error response as JSON', errorString, parseError);				errorObj = {message:errorString};			}			console.warn(promiseId + ' Reject', errorString);			callbacksById[promiseId].reject(new FMPromiseError(errorObj));			return delete callbacksById[promiseId];		},	}}();]]></Data>
							</StyledText>
						</Cell>
					</Row>
					<Row membercount="11" id="2">
						<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[CC2CF34E-6CA6-469B-BA53-A210E9703AD5]]></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[09-12-2021 06:55:12 PM]]></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-template.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">	<script>"use strict";const fmPromise=function(){let e=0;const t={},r={},n=new Promise((e,t)=>{let r=100;const n=window.setInterval(()=>{window.FileMaker?(window.clearInterval(n),e(window.FileMaker)):r--||(window.clearInterval(n),t("No window.FileMaker object was loaded after polling timeout."))},10)});function o(e){Object.assign(this,e),this.message=e.message||"Unknown error",this.name=this.constructor.name}return o.prototype=new Error,o.prototype.toString=function(){return this.message+(this.code?" ("+this.code+")":"")},{webViewerName:document.$FMP_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer",async configuration(e){let t=Object.entries(e).reduce((e,t)=>(e[t[0]]=t[1].value,e),{});await n;const r=await this.executeSql("select configuration from fmPromiseWebViewer where id=?",this.webViewerName);if(r.length&&r[0][0])try{const n=JSON.parse(r[0][0]);let o=Object.entries(e).filter(e=>{let[t,r]=e;return"string"==typeof r&&(r={type:r}),!1!==r.required&&"boolean"!==r.type&&!n[t]});if(0===o.length)return n;console.warn("Config is out of date",r,"missing key(s)",o),Object.assign(t,n)}catch(e){console.error("Could not parse",r,e)}await fmPromise.loadModule("fm-promise-config-form.js");const o=await fmPromiseConfiguration.show(e,t);return fmPromise.performScript("fmPromise.configure",o)},async loadModule(e){if(!r[e])return r[e]=!0,new Promise(async(t,r)=>{const n=document.createElement("script");if(n.type="text/javascript",0===e.indexOf("http"))n.src=src,n.onload=function(){t(src)};else{const s=await this.executeSql("select coalesce(sourceMinified, source) from fmPromiseModule where filename=?",e);s.length||r(new o({message:"Unable to locate fmPromise module "+e})),n.text=s[0][0],t(s)}document.head.appendChild(n)})},performScript(r,o=null,s={runningScript:0,alwaysReturnString:!1}){o&&"string"!=typeof o&&(o=JSON.stringify(o));const i=s.runningScript||0,a=++e;return n.then(e=>new Promise((n,s)=>{const c=this.webViewerName,l=JSON.stringify({scriptName:r,promiseId:a,webViewerName:c});t[a]={resolve:n,reject:s},console.info(a+' Performing script "'+r+'" with param '+(o?o.length:0)+" bytes");const m=l+"\n"+o;0===i?e.PerformScript("fmPromise",m):e.PerformScriptWithOption("fmPromise",m,i)})).then(e=>{if(!s.alwaysReturnString&&e&&"{"===e[0]||"["===e[0])try{e=JSON.parse(e)}catch(t){console.warn("Unable to parse JSON result ",e,t)}return e})},evaluate(e,t={},r={}){const n="Let(["+Object.entries(t||{}).map(e=>e[0]+"="+JSON.stringify(e[1])).join(";")+"] ; "+e+")";return this.performScript("fmPromise.evaluate",n,r)},executeFileMakerDataAPI(e){return this.performScript("fmPromise.executeFileMakerDataAPI",e).then(e=>{if(e&&e.messages&&e.messages.length){if("0"!==e.messages[0].code)throw new o(e.messages[0]);return e.response}throw new o({code:-1,message:"Empty data API response"})})},async executeFileMakerDataAPIRecords(e){return(await this.executeFileMakerDataAPI(e)).data.map(e=>{const t=e.fieldData;return t.recordId=e.recordId,t.modId=e.modId,Object.assign(t,e.portalData),t})},executeSql(e,...t){const r=t.map(e=>" ; "+JSON.stringify(e)).join(""),n="|"+Math.random()+"|",o="~"+Math.random()+"~";return this.evaluate("ExecuteSQL("+JSON.stringify(e)+' ; "'+n+'" ; "'+o+'"'+r+" )",null,{alwaysReturnString:!0}).then(e=>0===e.length?[]:e.split(o).map(e=>e.split(n)))},executeSqlTemplate(e,...t){if(!Array.isArray(e)||t.length!==e.length-1)throw new o({code:-1,message:"Invalid template literal for executeSqlTemplate"});return this.executeSql(e.join("?"),t)},insertFromUrl(e,t=""){return this.performScript("fmPromise.insertFromURL",{url:e,curlOptions:t})},setFieldByName(e,t){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:e,value:t})},showCustomDialog(e,t,r="OK",n="",o=""){return this.performScript("fmPromise.showCustomDialog",{title:e,body:t,btn1:r,btn2:n,btn3:o}).then(function(e){return parseInt(e)})},_resolve:(e,r)=>(console.info(e+" Resolve with "+r.length+" chars"),t[e].resolve(r),delete t[e]),_reject(e,r){let n;try{n=JSON.parse(r)}catch(e){console.warn("Unable to parse error response as JSON",r,e),n={message:r}}return console.warn(e+" Reject",r),t[e].reject(new o(n)),delete t[e]}}}();</script>	<script>		'use strict';		/* SAMPLE FUNCTIONALITY, REPLACE WITH YOUR OWN CODE */		// STRONGLY RECOMMENDED: enable Safari dev tools debugging in your Web Viewers as described at:		// https://blog.beezwax.net/2020/05/26/how-to-enable-webkit-and-javascript-debugging-in-filemaker-web-viewers/		window.onload = async function() {			const calc = 'Get(DocumentsPath) & "fmPromise/" & get ( FileName ) & "/"';			const path = await fmPromise.evaluate(calc);			document.body.innerHTML = document.body.innerHTML.replaceAll('$DocumentsPath', path);		}	</script>	<style>html,body{height:100%;margin:0;padding:0}p,li,div,h1,h2,h3,h4,h5{font-family:Arial,Helvetica,sans-serif;color:#707070}</style></head><body><h3>360Works FMPromise Add-On</h3><p>This is your new module! It has been written to <code>$DocumentsPath</code> </p><p>Edit the document there, and click <strong>Refresh</strong> to see your changes in the Web Viewer. Then use the <strong>Package</strong> button to insert the module contents to the corresponding <strong>fmPromiseModule</strong> record. Don't forget to package your file before deploying! Other users will not have access to your local development-mode file.</p><p>Read the <a href="https://360works.com/fmpromise" target="_blank">documentation</a>.</p></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[1]]></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[The HTML template for newly created HTML files. Includes links to fm-reset.css and fm-promise.js]]></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 src="fm-promise.js"></script>
	<script>
		'use strict';

		/* SAMPLE FUNCTIONALITY, REPLACE WITH YOUR OWN CODE */

		// STRONGLY RECOMMENDED: enable Safari dev tools debugging in your Web Viewers as described at:
		// https://blog.beezwax.net/2020/05/26/how-to-enable-webkit-and-javascript-debugging-in-filemaker-web-viewers/

		window.onload = async function() {
			const calc = 'Get(DocumentsPath) & "fmPromise/" & get ( FileName ) & "/"';
			const path = await fmPromise.evaluate(calc);
			document.body.innerHTML = document.body.innerHTML.replaceAll('$DocumentsPath', path);
		}
	</script>
	<style>
		html, body {height: 100%;margin: 0;padding: 0;}
		p, li, div, h1, h2, h3, h4, h5 {font-family: Arial, Helvetica, sans-serif;color: #707070;}
	</style>
</head>
<body>

<h3>360Works FMPromise Add-On</h3>

<p>This is your new module! It has been written to <code>$DocumentsPath</code> </p>

<p>Edit the document there, and click <strong>Refresh</strong> to see your changes in the Web Viewer. Then use the <strong>Package</strong> button to insert the module contents to the corresponding <strong>fmPromiseModule</strong> record. Don't forget to package your file before deploying! Other users will not have access to your local development-mode file.</p>

<p>Read the <a href="https://360works.com/fmpromise" target="_blank">documentation</a>.</p>

</body>
</html>
]]></Data>
							</StyledText>
						</Cell>
					</Row>
					<Row membercount="11" id="3">
						<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[AF3EFED1-248B-4A68-AAF0-AFA716F7E5FB]]></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[09-12-2021 06:55:16 PM]]></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[data-api-example.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">	<script>"use strict";const fmPromise=function(){let e=0;const t={},r={},n=new Promise((e,t)=>{let r=100;const n=window.setInterval(()=>{window.FileMaker?(window.clearInterval(n),e(window.FileMaker)):r--||(window.clearInterval(n),t("No window.FileMaker object was loaded after polling timeout."))},10)});function o(e){Object.assign(this,e),this.message=e.message||"Unknown error",this.name=this.constructor.name}return o.prototype=new Error,o.prototype.toString=function(){return this.message+(this.code?" ("+this.code+")":"")},{webViewerName:document.$FMP_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer",async configuration(e){let t=Object.entries(e).reduce((e,t)=>(e[t[0]]=t[1].value,e),{});await n;const r=await this.executeSql("select configuration from fmPromiseWebViewer where id=?",this.webViewerName);if(r.length&&r[0][0])try{const n=JSON.parse(r[0][0]);let o=Object.entries(e).filter(e=>{let[t,r]=e;return"string"==typeof r&&(r={type:r}),!1!==r.required&&"boolean"!==r.type&&!n[t]});if(0===o.length)return n;console.warn("Config is out of date",r,"missing key(s)",o),Object.assign(t,n)}catch(e){console.error("Could not parse",r,e)}await fmPromise.loadModule("fm-promise-config-form.js");const o=await fmPromiseConfiguration.show(e,t);return fmPromise.performScript("fmPromise.configure",o)},async loadModule(e){if(!r[e])return r[e]=!0,new Promise(async(t,r)=>{const n=document.createElement("script");if(n.type="text/javascript",0===e.indexOf("http"))n.src=src,n.onload=function(){t(src)};else{const s=await this.executeSql("select coalesce(sourceMinified, source) from fmPromiseModule where filename=?",e);s.length||r(new o({message:"Unable to locate fmPromise module "+e})),n.text=s[0][0],t(s)}document.head.appendChild(n)})},performScript(r,o=null,s={runningScript:0,alwaysReturnString:!1}){o&&"string"!=typeof o&&(o=JSON.stringify(o));const i=s.runningScript||0,a=++e;return n.then(e=>new Promise((n,s)=>{const c=this.webViewerName,l=JSON.stringify({scriptName:r,promiseId:a,webViewerName:c});t[a]={resolve:n,reject:s},console.info(a+' Performing script "'+r+'" with param '+(o?o.length:0)+" bytes");const m=l+"\n"+o;0===i?e.PerformScript("fmPromise",m):e.PerformScriptWithOption("fmPromise",m,i)})).then(e=>{if(!s.alwaysReturnString&&e&&"{"===e[0]||"["===e[0])try{e=JSON.parse(e)}catch(t){console.warn("Unable to parse JSON result ",e,t)}return e})},evaluate(e,t={},r={}){const n="Let(["+Object.entries(t||{}).map(e=>e[0]+"="+JSON.stringify(e[1])).join(";")+"] ; "+e+")";return this.performScript("fmPromise.evaluate",n,r)},executeFileMakerDataAPI(e){return this.performScript("fmPromise.executeFileMakerDataAPI",e).then(e=>{if(e&&e.messages&&e.messages.length){if("0"!==e.messages[0].code)throw new o(e.messages[0]);return e.response}throw new o({code:-1,message:"Empty data API response"})})},async executeFileMakerDataAPIRecords(e){return(await this.executeFileMakerDataAPI(e)).data.map(e=>{const t=e.fieldData;return t.recordId=e.recordId,t.modId=e.modId,Object.assign(t,e.portalData),t})},executeSql(e,...t){const r=t.map(e=>" ; "+JSON.stringify(e)).join(""),n="|"+Math.random()+"|",o="~"+Math.random()+"~";return this.evaluate("ExecuteSQL("+JSON.stringify(e)+' ; "'+n+'" ; "'+o+'"'+r+" )",null,{alwaysReturnString:!0}).then(e=>0===e.length?[]:e.split(o).map(e=>e.split(n)))},executeSqlTemplate(e,...t){if(!Array.isArray(e)||t.length!==e.length-1)throw new o({code:-1,message:"Invalid template literal for executeSqlTemplate"});return this.executeSql(e.join("?"),t)},insertFromUrl(e,t=""){return this.performScript("fmPromise.insertFromURL",{url:e,curlOptions:t})},setFieldByName(e,t){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:e,value:t})},showCustomDialog(e,t,r="OK",n="",o=""){return this.performScript("fmPromise.showCustomDialog",{title:e,body:t,btn1:r,btn2:n,btn3:o}).then(function(e){return parseInt(e)})},_resolve:(e,r)=>(console.info(e+" Resolve with "+r.length+" chars"),t[e].resolve(r),delete t[e]),_reject(e,r){let n;try{n=JSON.parse(r)}catch(e){console.warn("Unable to parse error response as JSON",r,e),n={message:r}}return console.warn(e+" Reject",r),t[e].reject(new o(n)),delete t[e]}}}();</script>	<script>		'use strict';		window.onload = async function() {			const request = {layouts: 'fmPromiseModule', query: [{filename: '*html'}], sort: [{fieldName: 'filename', sortOrder: 'ascend'}]};			const found = await fmPromise.executeFileMakerDataAPI(request);			const items = found.data.map(row => `<li title="${row.fieldData.id}">${row.fieldData.filename}</li>`).join('');			document.getElementById('result').innerHTML = '<ul>' + items + '</ul>';		}	</script>	<style>html,body{height:100%;margin:0;padding:0}p,li,div,h1,h2,h3,h4,h5{font-family:Arial,Helvetica,sans-serif;color:#707070}</style></head><body><h3>fmPromise Modules, as loaded from data API metadata query</h3><div id="result" style="color:#369;">Loading…</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[Fetches all "html" records from the fmPromiseModule table using the data API. Then it builds an unordered list of the `filename` field values]]></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 src="fm-promise.js"></script>
	<script>
		'use strict';
		window.onload = async function() {
			const request = {layouts: 'fmPromiseModule', query: [{filename: '*html'}], sort: [{fieldName: 'filename', sortOrder: 'ascend'}]};
			const found = await fmPromise.executeFileMakerDataAPI(request);
			const items = found.data.map(row => `<li title="${row.fieldData.id}">${row.fieldData.filename}</li>`).join('');
			document.getElementById('result').innerHTML = '<ul>' + items + '</ul>';
		}
	</script>
	<style>
		html, body {height: 100%;margin: 0;padding: 0;}
		p, li, div, h1, h2, h3, h4, h5 {font-family: Arial, Helvetica, sans-serif;color: #707070;}
	</style>
</head>
<body>
<h3>fmPromise Modules, as loaded from data API metadata query</h3>
<div id="result" style="color:#369;">Loading&hellip;</div>
</body>
</html>
]]></Data>
							</StyledText>
						</Cell>
					</Row>
					<Row membercount="11" id="4">
						<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[2DBF1570-2F70-4ECE-B49F-51E1A0750E74]]></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[09-12-2021 06:55:17 PM]]></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[evaluate-example.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:0}p,li,div,h1,h2,h3,h4,h5{font-family:Arial,Helvetica,sans-serif;color:#707070}label{font-weight:bold}#content{display:flex;flex-direction:column;height:100vh}textarea{flex-grow:1}#evalControls{float:right}textarea.error{background-color:#ffe0e5;font-weight:bold;color:red}</style>	<script>"use strict";const fmPromise=function(){let e=0;const t={},r={},n=new Promise((e,t)=>{let r=100;const n=window.setInterval(()=>{window.FileMaker?(window.clearInterval(n),e(window.FileMaker)):r--||(window.clearInterval(n),t("No window.FileMaker object was loaded after polling timeout."))},10)});function o(e){Object.assign(this,e),this.message=e.message||"Unknown error",this.name=this.constructor.name}return o.prototype=new Error,o.prototype.toString=function(){return this.message+(this.code?" ("+this.code+")":"")},{webViewerName:document.$FMP_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer",async configuration(e){let t=Object.entries(e).reduce((e,t)=>(e[t[0]]=t[1].value,e),{});await n;const r=await this.executeSql("select configuration from fmPromiseWebViewer where id=?",this.webViewerName);if(r.length&&r[0][0])try{const n=JSON.parse(r[0][0]);let o=Object.entries(e).filter(e=>{let[t,r]=e;return"string"==typeof r&&(r={type:r}),!1!==r.required&&"boolean"!==r.type&&!n[t]});if(0===o.length)return n;console.warn("Config is out of date",r,"missing key(s)",o),Object.assign(t,n)}catch(e){console.error("Could not parse",r,e)}await fmPromise.loadModule("fm-promise-config-form.js");const o=await fmPromiseConfiguration.show(e,t);return fmPromise.performScript("fmPromise.configure",o)},async loadModule(e){if(!r[e])return r[e]=!0,new Promise(async(t,r)=>{const n=document.createElement("script");if(n.type="text/javascript",0===e.indexOf("http"))n.src=src,n.onload=function(){t(src)};else{const s=await this.executeSql("select coalesce(sourceMinified, source) from fmPromiseModule where filename=?",e);s.length||r(new o({message:"Unable to locate fmPromise module "+e})),n.text=s[0][0],t(s)}document.head.appendChild(n)})},performScript(r,o=null,s={runningScript:0,alwaysReturnString:!1}){o&&"string"!=typeof o&&(o=JSON.stringify(o));const i=s.runningScript||0,a=++e;return n.then(e=>new Promise((n,s)=>{const c=this.webViewerName,l=JSON.stringify({scriptName:r,promiseId:a,webViewerName:c});t[a]={resolve:n,reject:s},console.info(a+' Performing script "'+r+'" with param '+(o?o.length:0)+" bytes");const m=l+"\n"+o;0===i?e.PerformScript("fmPromise",m):e.PerformScriptWithOption("fmPromise",m,i)})).then(e=>{if(!s.alwaysReturnString&&e&&"{"===e[0]||"["===e[0])try{e=JSON.parse(e)}catch(t){console.warn("Unable to parse JSON result ",e,t)}return e})},evaluate(e,t={},r={}){const n="Let(["+Object.entries(t||{}).map(e=>e[0]+"="+JSON.stringify(e[1])).join(";")+"] ; "+e+")";return this.performScript("fmPromise.evaluate",n,r)},executeFileMakerDataAPI(e){return this.performScript("fmPromise.executeFileMakerDataAPI",e).then(e=>{if(e&&e.messages&&e.messages.length){if("0"!==e.messages[0].code)throw new o(e.messages[0]);return e.response}throw new o({code:-1,message:"Empty data API response"})})},async executeFileMakerDataAPIRecords(e){return(await this.executeFileMakerDataAPI(e)).data.map(e=>{const t=e.fieldData;return t.recordId=e.recordId,t.modId=e.modId,Object.assign(t,e.portalData),t})},executeSql(e,...t){const r=t.map(e=>" ; "+JSON.stringify(e)).join(""),n="|"+Math.random()+"|",o="~"+Math.random()+"~";return this.evaluate("ExecuteSQL("+JSON.stringify(e)+' ; "'+n+'" ; "'+o+'"'+r+" )",null,{alwaysReturnString:!0}).then(e=>0===e.length?[]:e.split(o).map(e=>e.split(n)))},executeSqlTemplate(e,...t){if(!Array.isArray(e)||t.length!==e.length-1)throw new o({code:-1,message:"Invalid template literal for executeSqlTemplate"});return this.executeSql(e.join("?"),t)},insertFromUrl(e,t=""){return this.performScript("fmPromise.insertFromURL",{url:e,curlOptions:t})},setFieldByName(e,t){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:e,value:t})},showCustomDialog(e,t,r="OK",n="",o=""){return this.performScript("fmPromise.showCustomDialog",{title:e,body:t,btn1:r,btn2:n,btn3:o}).then(function(e){return parseInt(e)})},_resolve:(e,r)=>(console.info(e+" Resolve with "+r.length+" chars"),t[e].resolve(r),delete t[e]),_reject(e,r){let n;try{n=JSON.parse(r)}catch(e){console.warn("Unable to parse error response as JSON",r,e),n={message:r}}return console.warn(e+" Reject",r),t[e].reject(new o(n)),delete t[e]}}}();</script>	<script>		'use strict';		async function go() {			let resultArea = document.getElementById('result');			try {				const exp = document.getElementById('expression').value;				resultArea.value = await fmPromise.evaluate(exp);				resultArea.classList.remove('error');			} catch (e) {				resultArea.value = 'Error: ' + JSON.stringify(e, null, 4);				resultArea.classList.add('error');			}		}	</script></head><body><div id="content">	<label for="expression">Expression</label>	<textarea cols="30" id="expression" name="expression" rows="10">"Wake up, " &amp; Get ( Username ) &amp; ", it's " &amp; get(CurrentTime) &amp; "!"</textarea>	<div>		<div id="evalControls">			<!--			<label for="autoEval"><input id="autoEval" name="autoEval" type="checkbox">Automatically Evaluate</label>-->			<button onclick="go()">Evaluate</button>		</div>		<label for="result">Result</label>	</div>	<textarea cols="30" id="result" name="result" rows="10"></textarea></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[This is a simple re-implementation of the FileMaker "Edit Expression" dialog as found in the Data 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">
	<style>
		html, body {height: 100%;margin: 0;padding: 0;}
		p, li, div, h1, h2, h3, h4, h5 {font-family: Arial, Helvetica, sans-serif;color: #707070;}

        label {
            font-weight: bold;
        }

        #content {
            display: flex;
            flex-direction: column;
            height: 100vh;
        }

        textarea {
            flex-grow: 1;
        }

        #evalControls {
            float: right;
        }

        textarea.error {
            background-color: #ffe0e5;
			font-weight: bold;
            color: red;
        }
	</style>
	<script src="fm-promise.js"></script>
	<script>
		'use strict';

		async function go() {
			let resultArea = document.getElementById('result');
			try {
				const exp = document.getElementById('expression').value;
				resultArea.value = await fmPromise.evaluate(exp);
				resultArea.classList.remove('error');
			} catch (e) {
				resultArea.value = 'Error: ' + JSON.stringify(e, null, 4);
				resultArea.classList.add('error');
			}
		}
	</script>
</head>
<body>

<div id="content">
	<label for="expression">Expression</label>
	<textarea cols="30" id="expression" name="expression" rows="10">"Wake up, " & Get ( Username ) & ", it's " & get(CurrentTime) & "!"</textarea>
	<div>
		<div id="evalControls">
			<!--			<label for="autoEval"><input id="autoEval" name="autoEval" type="checkbox">Automatically Evaluate</label>-->
			<button onclick="go()">Evaluate</button>
		</div>
		<label for="result">Result</label>
	</div>
	<textarea cols="30" id="result" name="result" rows="10"></textarea>
</div>

</body>
</html>
]]></Data>
							</StyledText>
						</Cell>
					</Row>
					<Row membercount="11" id="5">
						<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[09-12-2021 06:55:25 PM]]></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-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">	<script>"use strict";const fmPromise=function(){let e=0;const t={},r={},n=new Promise((e,t)=>{let r=100;const n=window.setInterval(()=>{window.FileMaker?(window.clearInterval(n),e(window.FileMaker)):r--||(window.clearInterval(n),t("No window.FileMaker object was loaded after polling timeout."))},10)});function o(e){Object.assign(this,e),this.message=e.message||"Unknown error",this.name=this.constructor.name}return o.prototype=new Error,o.prototype.toString=function(){return this.message+(this.code?" ("+this.code+")":"")},{webViewerName:document.$FMP_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer",async configuration(e){let t=Object.entries(e).reduce((e,t)=>(e[t[0]]=t[1].value,e),{});await n;const r=await this.executeSql("select configuration from fmPromiseWebViewer where id=?",this.webViewerName);if(r.length&&r[0][0])try{const n=JSON.parse(r[0][0]);let o=Object.entries(e).filter(e=>{let[t,r]=e;return"string"==typeof r&&(r={type:r}),!1!==r.required&&"boolean"!==r.type&&!n[t]});if(0===o.length)return n;console.warn("Config is out of date",r,"missing key(s)",o),Object.assign(t,n)}catch(e){console.error("Could not parse",r,e)}await fmPromise.loadModule("fm-promise-config-form.js");const o=await fmPromiseConfiguration.show(e,t);return fmPromise.performScript("fmPromise.configure",o)},async loadModule(e){if(!r[e])return r[e]=!0,new Promise(async(t,r)=>{const n=document.createElement("script");if(n.type="text/javascript",0===e.indexOf("http"))n.src=src,n.onload=function(){t(src)};else{const s=await this.executeSql("select coalesce(sourceMinified, source) from fmPromiseModule where filename=?",e);s.length||r(new o({message:"Unable to locate fmPromise module "+e})),n.text=s[0][0],t(s)}document.head.appendChild(n)})},performScript(r,o=null,s={runningScript:0,alwaysReturnString:!1}){o&&"string"!=typeof o&&(o=JSON.stringify(o));const i=s.runningScript||0,a=++e;return n.then(e=>new Promise((n,s)=>{const c=this.webViewerName,l=JSON.stringify({scriptName:r,promiseId:a,webViewerName:c});t[a]={resolve:n,reject:s},console.info(a+' Performing script "'+r+'" with param '+(o?o.length:0)+" bytes");const m=l+"\n"+o;0===i?e.PerformScript("fmPromise",m):e.PerformScriptWithOption("fmPromise",m,i)})).then(e=>{if(!s.alwaysReturnString&&e&&"{"===e[0]||"["===e[0])try{e=JSON.parse(e)}catch(t){console.warn("Unable to parse JSON result ",e,t)}return e})},evaluate(e,t={},r={}){const n="Let(["+Object.entries(t||{}).map(e=>e[0]+"="+JSON.stringify(e[1])).join(";")+"] ; "+e+")";return this.performScript("fmPromise.evaluate",n,r)},executeFileMakerDataAPI(e){return this.performScript("fmPromise.executeFileMakerDataAPI",e).then(e=>{if(e&&e.messages&&e.messages.length){if("0"!==e.messages[0].code)throw new o(e.messages[0]);return e.response}throw new o({code:-1,message:"Empty data API response"})})},async executeFileMakerDataAPIRecords(e){return(await this.executeFileMakerDataAPI(e)).data.map(e=>{const t=e.fieldData;return t.recordId=e.recordId,t.modId=e.modId,Object.assign(t,e.portalData),t})},executeSql(e,...t){const r=t.map(e=>" ; "+JSON.stringify(e)).join(""),n="|"+Math.random()+"|",o="~"+Math.random()+"~";return this.evaluate("ExecuteSQL("+JSON.stringify(e)+' ; "'+n+'" ; "'+o+'"'+r+" )",null,{alwaysReturnString:!0}).then(e=>0===e.length?[]:e.split(o).map(e=>e.split(n)))},executeSqlTemplate(e,...t){if(!Array.isArray(e)||t.length!==e.length-1)throw new o({code:-1,message:"Invalid template literal for executeSqlTemplate"});return this.executeSql(e.join("?"),t)},insertFromUrl(e,t=""){return this.performScript("fmPromise.insertFromURL",{url:e,curlOptions:t})},setFieldByName(e,t){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:e,value:t})},showCustomDialog(e,t,r="OK",n="",o=""){return this.performScript("fmPromise.showCustomDialog",{title:e,body:t,btn1:r,btn2:n,btn3:o}).then(function(e){return parseInt(e)})},_resolve:(e,r)=>(console.info(e+" Resolve with "+r.length+" chars"),t[e].resolve(r),delete t[e]),_reject(e,r){let n;try{n=JSON.parse(r)}catch(e){console.warn("Unable to parse error response as JSON",r,e),n={message:r}}return console.warn(e+" Reject",r),t[e].reject(new o(n)),delete t[e]}}}();</script>	<script>		'use strict';		async function submitModuleFilename() {			let filename = document.getElementById('filename').value.toLowerCase();			if (!filename.endsWith('.html')) {				filename += '.html';			}			try {				await fmPromise.performScript('fmPromise.createOrRefreshModule', {filename: filename, webViewerName: fmPromise.webViewerName});				document.getElementById('myForm').style.display = 'none';				document.getElementById('success').style.display = 'block';			} catch (e) {				document.getElementById('error').innerText = 'Error: ' + ( e.message || e );			}		}		window.onload = async function() {			const existing = await fmPromise.executeSql("select filename from fmPromiseModule where includeInNewFiles=0 and filename not like 'fm-promise%'")			document.getElementById('existing-files').append(...existing.map( f => {				let option = document.createElement('option');				option.value = f[0];				return option;			}))		}	</script>	<style>html,body{height:100%;margin:0;padding:0}p,li,div,h1,h2,h3,h4,h5{font-family:Arial,Helvetica,sans-serif;color:#707070}input{width:22em}#success{color:darkgreen;font-weight:bold;display:none}#error{color:darkred;font-weight:bold}</style></head><body><div id="app">	<h3>360Works FMPromise Add-On</h3>	<p>You have successfully added a <a href="https://360works.com/fmpromise" target="_blank">360Works FMPromise</a> add-on web viewer to your layout! 😊</p>	<p>This web viewer can display an HTML page which you create. Enter a <strong>unique</strong> name for the HTML file to display:</p>	<form id="myForm" onsubmit="event.preventDefault();submitModuleFilename()" role="form">		<div class="form-control">			<label for="filename">Unique HTML Filename</label>			<input id="filename" name="filename" list="existing-files" placeholder="" required="">			<datalist id="existing-files"></datalist>			<button type="submit">Continue</button>		</div>		<div id="error"></div>	</form>	<div id="success">		<h3>Your fmPromise module has been registered!</h3>		Reload this web viewer to show the new module.	</div></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 src="fm-promise.js"></script>
	<script>
		'use strict';

		async function submitModuleFilename() {
			let filename = document.getElementById('filename').value.toLowerCase();
			if (!filename.endsWith('.html')) {
				filename += '.html';
			}
			try {
				await fmPromise.performScript('fmPromise.createOrRefreshModule', {filename: filename, webViewerName: fmPromise.webViewerName});
				document.getElementById('myForm').style.display = 'none';
				document.getElementById('success').style.display = 'block';
			} catch (e) {
				document.getElementById('error').innerText = 'Error: ' + ( e.message || e );
			}
		}

		window.onload = async function() {
			const existing = await fmPromise.executeSql("select filename from fmPromiseModule where includeInNewFiles=0 and filename not like 'fm-promise%'")
			document.getElementById('existing-files').append(...existing.map( f => {
				let option = document.createElement('option');
				option.value = f[0];
				return option;
			}))
		}
	</script>
	<style>
		html, body {height: 100%;margin: 0;padding: 0;}
		p, li, div, h1, h2, h3, h4, h5 {font-family: Arial, Helvetica, sans-serif;color: #707070;}

		input {
			width:22em;
		}
        #success {
            color: darkgreen;
            font-weight: bold;
            display: none;
        }

        #error {
            color: darkred;
            font-weight: bold;
        }
	</style>
</head>
<body>
<div id="app">
	<h3>360Works FMPromise Add-On</h3>
	<p>You have successfully added a <a href="https://360works.com/fmpromise" target="_blank">360Works FMPromise</a> add-on web viewer to your layout! 😊</p>
	<p>This web viewer can display an HTML page which you create. Enter a <strong>unique</strong> name for the HTML file to display:</p>

	<form id="myForm" onsubmit="event.preventDefault();submitModuleFilename()" role="form">
		<div class="form-control">
			<label for="filename">Unique HTML Filename</label>
			<input id="filename" name="filename" list="existing-files" placeholder="" required/>
			<datalist id="existing-files"></datalist>

			<button type="submit">Continue</button>
		</div>
		<div id="error"></div>
	</form>
	<div id="success">
		<h3>Your fmPromise module has been registered!</h3>
		Reload this web viewer to show the new module.
	</div>
</div>
</body>
</html>
]]></Data>
							</StyledText>
						</Cell>
					</Row>
					<Row membercount="11" id="6">
						<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[5E39C845-3B5C-47F1-A838-0A0442EE8F87]]></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[09-12-2021 06:55:28 PM]]></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-packager.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">	<script>"use strict";const fmPromise=function(){let e=0;const t={},r={},n=new Promise((e,t)=>{let r=100;const n=window.setInterval(()=>{window.FileMaker?(window.clearInterval(n),e(window.FileMaker)):r--||(window.clearInterval(n),t("No window.FileMaker object was loaded after polling timeout."))},10)});function o(e){Object.assign(this,e),this.message=e.message||"Unknown error",this.name=this.constructor.name}return o.prototype=new Error,o.prototype.toString=function(){return this.message+(this.code?" ("+this.code+")":"")},{webViewerName:document.$FMP_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer",async configuration(e){let t=Object.entries(e).reduce((e,t)=>(e[t[0]]=t[1].value,e),{});await n;const r=await this.executeSql("select configuration from fmPromiseWebViewer where id=?",this.webViewerName);if(r.length&&r[0][0])try{const n=JSON.parse(r[0][0]);let o=Object.entries(e).filter(e=>{let[t,r]=e;return"string"==typeof r&&(r={type:r}),!1!==r.required&&"boolean"!==r.type&&!n[t]});if(0===o.length)return n;console.warn("Config is out of date",r,"missing key(s)",o),Object.assign(t,n)}catch(e){console.error("Could not parse",r,e)}await fmPromise.loadModule("fm-promise-config-form.js");const o=await fmPromiseConfiguration.show(e,t);return fmPromise.performScript("fmPromise.configure",o)},async loadModule(e){if(!r[e])return r[e]=!0,new Promise(async(t,r)=>{const n=document.createElement("script");if(n.type="text/javascript",0===e.indexOf("http"))n.src=src,n.onload=function(){t(src)};else{const s=await this.executeSql("select coalesce(sourceMinified, source) from fmPromiseModule where filename=?",e);s.length||r(new o({message:"Unable to locate fmPromise module "+e})),n.text=s[0][0],t(s)}document.head.appendChild(n)})},performScript(r,o=null,s={runningScript:0,alwaysReturnString:!1}){o&&"string"!=typeof o&&(o=JSON.stringify(o));const i=s.runningScript||0,a=++e;return n.then(e=>new Promise((n,s)=>{const c=this.webViewerName,l=JSON.stringify({scriptName:r,promiseId:a,webViewerName:c});t[a]={resolve:n,reject:s},console.info(a+' Performing script "'+r+'" with param '+(o?o.length:0)+" bytes");const m=l+"\n"+o;0===i?e.PerformScript("fmPromise",m):e.PerformScriptWithOption("fmPromise",m,i)})).then(e=>{if(!s.alwaysReturnString&&e&&"{"===e[0]||"["===e[0])try{e=JSON.parse(e)}catch(t){console.warn("Unable to parse JSON result ",e,t)}return e})},evaluate(e,t={},r={}){const n="Let(["+Object.entries(t||{}).map(e=>e[0]+"="+JSON.stringify(e[1])).join(";")+"] ; "+e+")";return this.performScript("fmPromise.evaluate",n,r)},executeFileMakerDataAPI(e){return this.performScript("fmPromise.executeFileMakerDataAPI",e).then(e=>{if(e&&e.messages&&e.messages.length){if("0"!==e.messages[0].code)throw new o(e.messages[0]);return e.response}throw new o({code:-1,message:"Empty data API response"})})},async executeFileMakerDataAPIRecords(e){return(await this.executeFileMakerDataAPI(e)).data.map(e=>{const t=e.fieldData;return t.recordId=e.recordId,t.modId=e.modId,Object.assign(t,e.portalData),t})},executeSql(e,...t){const r=t.map(e=>" ; "+JSON.stringify(e)).join(""),n="|"+Math.random()+"|",o="~"+Math.random()+"~";return this.evaluate("ExecuteSQL("+JSON.stringify(e)+' ; "'+n+'" ; "'+o+'"'+r+" )",null,{alwaysReturnString:!0}).then(e=>0===e.length?[]:e.split(o).map(e=>e.split(n)))},executeSqlTemplate(e,...t){if(!Array.isArray(e)||t.length!==e.length-1)throw new o({code:-1,message:"Invalid template literal for executeSqlTemplate"});return this.executeSql(e.join("?"),t)},insertFromUrl(e,t=""){return this.performScript("fmPromise.insertFromURL",{url:e,curlOptions:t})},setFieldByName(e,t){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:e,value:t})},showCustomDialog(e,t,r="OK",n="",o=""){return this.performScript("fmPromise.showCustomDialog",{title:e,body:t,btn1:r,btn2:n,btn3:o}).then(function(e){return parseInt(e)})},_resolve:(e,r)=>(console.info(e+" Resolve with "+r.length+" chars"),t[e].resolve(r),delete t[e]),_reject(e,r){let n;try{n=JSON.parse(r)}catch(e){console.warn("Unable to parse error response as JSON",r,e),n={message:r}}return console.warn(e+" Reject",r),t[e].reject(new o(n)),delete t[e]}}}();</script>	<title>fmPromise packager</title>	<script>		'use strict';		fmPromise.webViewerName = 'packagerWebViewer';		/**		 * Given an html file, this uses fmPromise to get the file contents, parse it as DOM, and inline any external references		 */		async function packageModule(source, fmFieldNameToSet, didOpenWindow, baseUri) {			function byteLengthFormat(num) {				return '<code>' + new Intl.NumberFormat().format(num) + '</code> bytes';			}			const listItems = [];			listItems.push('Original file size: ' + byteLengthFormat(source.length));			const doc = new DOMParser().parseFromString(source, 'text/html');			// adjust baseURI so relative paths can be packaged by InsertFromURL			const base = doc.createElement('base');			base.href = baseUri;			doc.head.appendChild(base);			for (const s of [...doc.getElementsByTagName('script')]) {				if (s.src) {					let url = s.getAttribute('src'); // `s.getAttribute('src')` returns a relative src, unlike `s.src` which is canonical					let packagePolicy = s.getAttribute('data-package');					console.log('Attempting to inline script src=' + url);					// is there a minified/non-minified version of this resource in fmPromise? Use that first					let sourceFromDb = await fmPromise.executeSql(							'select sourceMinified, source from fmPromiseModule where lower(filename) = lower(?)',							s.getAttribute('src').replace(/^\.\//, '')); // convert `./relative-path.js` to `relative-path.js` when querying module					if (packagePolicy === 'omit') {						// get rid of the tag						listItems.push('<code>' + url + '</code> omitted');						s.remove();						continue;					} else if (packagePolicy === 'leave') {						// leave it as an external resource						listItems.push('<code>' + url + '</code> "leave" in place');						continue;					} else if (sourceFromDb && sourceFromDb.length) {						s.innerHTML = sourceFromDb[0][0] || sourceFromDb[0][1];					} else {						// check for a minified version of the script at the location specified by the src attribute						url = s.src.replace(/.js$/, '.min.js'); // try a minified version first						console.log('  looking for ' + url);						try {							s.innerHTML = await fmPromise.insertFromUrl(url);						} catch (e) {							url = s.src; // no min.js found, use the non-minified version							try {								s.innerHTML = await fmPromise.insertFromUrl(s.src);							} catch (e) {								listItems.push('WARNING! <code>' + url + '</code> could not be fetched');								const choice = await fmPromise.showCustomDialog('Package Error',										'Unable to fetch ' + url,										'Abort',										'Ignore'								);								if (choice === 0) {									throw('User canceled');								} else {									continue;								}							}						}					}					listItems.push('<code>' + url + '</code> inlined: ' + byteLengthFormat(s.innerHTML.length));					s.removeAttribute('src');				}			}			for (const s of [...doc.getElementsByTagName('style')]) {				// minify inline styles				s.innerHTML = s.innerHTML						.replace(/([^0-9a-zA-Z.#])\s+/g, "$1")						.replace(/\s([^0-9a-zA-Z.#]+)/g, "$1")						.replace(/;}/g, "}")						.replace(/\/\*.*?\*\//g, "");			}			for (const s of [...doc.getElementsByTagName('link')]) {				if (s.href && s.rel === 'stylesheet') {					let cssHref = s.getAttribute('href');					let packagePolicy = s.getAttribute('data-fm-promise-package');					console.log('Inlining CSS ' + cssHref);					let styleNode = document.createElement('style');					const fromDb = await fmPromise.executeSql('select sourceMinified, source from fmPromiseModule where lower(filename)=lower(?)', cssHref);					if (packagePolicy === 'omit') {						// get rid of the tag						styleNode.remove();					} else if (packagePolicy === 'leave') {						// leave it external					} else if (fromDb.length) {						styleNode.innerHTML = fromDb[0][0] || fromDb[0][1];					} else {						cssHref = s.href;						styleNode.innerHTML = await fmPromise.insertFromUrl(cssHref);					}					s.replaceWith(styleNode);					listItems.push('<code>' + cssHref + '</code> inlined: ' + byteLengthFormat(styleNode.innerText.length));				}			}			// remove base			base.remove();			// output results			let packaged = '<!doctype html>\n' + doc.firstElementChild.outerHTML;			listItems.push('Final HTML is ' + byteLengthFormat(packaged.length));			const listHtml = listItems.map(s => '<li>' + s + '</li>').join('');			document.getElementById('output').innerHTML = `<h3>✅ Finished Packaging Module</h3>					<p><code>${fmFieldNameToSet}</code> now contains your minified, inlined module as a single HTML page.</p>					<p>You may now close this window.</p>					<ul>${listHtml}</ul>`;			await fmPromise.setFieldByName(fmFieldNameToSet, packaged);			return 'OK';		}	</script></head><body><p id="output">This module is used to package your HTML into a single-file web-viewer-friendly string, for storage in a FileMaker field.</p></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[This module is used to package your HTML into a single-file web-viewer-friendly string, for storage in a FileMaker field.]]></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 src="fm-promise.js"></script>
	<title>fmPromise packager</title>
	<script>
		'use strict';
		fmPromise.webViewerName = 'packagerWebViewer';

		/**
		 * Given an html file, this uses fmPromise to get the file contents, parse it as DOM, and inline any external references
		 */
		async function packageModule(source, fmFieldNameToSet, didOpenWindow, baseUri) {
			function byteLengthFormat(num) {
				return '<code>' + new Intl.NumberFormat().format(num) + '</code> bytes';
			}

			const listItems = [];
			listItems.push('Original file size: ' + byteLengthFormat(source.length));
			const doc = new DOMParser().parseFromString(source, 'text/html');
			// adjust baseURI so relative paths can be packaged by InsertFromURL
			const base = doc.createElement('base');
			base.href = baseUri;
			doc.head.appendChild(base);

			for (const s of [...doc.getElementsByTagName('script')]) {
				if (s.src) {
					let url = s.getAttribute('src'); // `s.getAttribute('src')` returns a relative src, unlike `s.src` which is canonical
					let packagePolicy = s.getAttribute('data-package');
					console.log('Attempting to inline script src=' + url);
					// is there a minified/non-minified version of this resource in fmPromise? Use that first
					let sourceFromDb = await fmPromise.executeSql(
							'select sourceMinified, source from fmPromiseModule where lower(filename) = lower(?)',
							s.getAttribute('src').replace(/^\.\//, '')); // convert `./relative-path.js` to `relative-path.js` when querying module
					if (packagePolicy === 'omit') {
						// get rid of the tag
						listItems.push('<code>' + url + '</code> omitted');
						s.remove();
						continue;
					} else if (packagePolicy === 'leave') {
						// leave it as an external resource
						listItems.push('<code>' + url + '</code> "leave" in place');
						continue;
					} else if (sourceFromDb && sourceFromDb.length) {
						s.innerHTML = sourceFromDb[0][0] || sourceFromDb[0][1];
					} else {
						// check for a minified version of the script at the location specified by the src attribute
						url = s.src.replace(/.js$/, '.min.js'); // try a minified version first
						console.log('  looking for ' + url);
						try {
							s.innerHTML = await fmPromise.insertFromUrl(url);
						} catch (e) {
							url = s.src; // no min.js found, use the non-minified version
							try {
								s.innerHTML = await fmPromise.insertFromUrl(s.src);
							} catch (e) {
								listItems.push('WARNING! <code>' + url + '</code> could not be fetched');
								const choice = await fmPromise.showCustomDialog('Package Error',
										'Unable to fetch ' + url,
										'Abort',
										'Ignore'
								);
								if (choice === 0) {
									throw('User canceled');
								} else {
									continue;
								}
							}
						}
					}
					listItems.push('<code>' + url + '</code> inlined: ' + byteLengthFormat(s.innerHTML.length));
					s.removeAttribute('src');
				}
			}
			for (const s of [...doc.getElementsByTagName('style')]) {
				// minify inline styles
				s.innerHTML = s.innerHTML
						.replace(/([^0-9a-zA-Z.#])\s+/g, "$1")
						.replace(/\s([^0-9a-zA-Z.#]+)/g, "$1")
						.replace(/;}/g, "}")
						.replace(/\/\*.*?\*\//g, "");
			}
			for (const s of [...doc.getElementsByTagName('link')]) {
				if (s.href && s.rel === 'stylesheet') {
					let cssHref = s.getAttribute('href');
					let packagePolicy = s.getAttribute('data-fm-promise-package');
					console.log('Inlining CSS ' + cssHref);
					let styleNode = document.createElement('style');
					const fromDb = await fmPromise.executeSql('select sourceMinified, source from fmPromiseModule where lower(filename)=lower(?)', cssHref);
					if (packagePolicy === 'omit') {
						// get rid of the tag
						styleNode.remove();
					} else if (packagePolicy === 'leave') {
						// leave it external
					} else if (fromDb.length) {
						styleNode.innerHTML = fromDb[0][0] || fromDb[0][1];
					} else {
						cssHref = s.href;
						styleNode.innerHTML = await fmPromise.insertFromUrl(cssHref);
					}
					s.replaceWith(styleNode);
					listItems.push('<code>' + cssHref + '</code> inlined: ' + byteLengthFormat(styleNode.innerText.length));
				}
			}
			// remove base
			base.remove();
			// output results
			let packaged = '<!doctype html>\n' + doc.firstElementChild.outerHTML;
			listItems.push('Final HTML is ' + byteLengthFormat(packaged.length));
			const listHtml = listItems.map(s => '<li>' + s + '</li>').join('');
			document.getElementById('output').innerHTML = `<h3>✅ Finished Packaging Module</h3>
					<p><code>${fmFieldNameToSet}</code> now contains your minified, inlined module as a single HTML page.</p>
					<p>You may now close this window.</p>
					<ul>${listHtml}</ul>`;
			await fmPromise.setFieldByName(fmFieldNameToSet, packaged);
			return 'OK';
		}

	</script>
</head>
<body>
<p id="output">This module is used to package your HTML into a single-file web-viewer-friendly string, for storage in a FileMaker field.</p>
</body>

</html>
]]></Data>
							</StyledText>
						</Cell>
					</Row>
					<Row membercount="11" id="7">
						<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[031AD491-5ADB-4735-B870-A59F7F31B811]]></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[08-04-2021 09:46:14 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-config-form.js]]></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["use strict";const fmPromiseConfiguration=function(){return{async show(e,t){const n=document.createElement("div");for(n.style.display="none";document.body.firstChild;)n.appendChild(document.body.firstChild);document.body.append(n);const a=(await fmPromise.executeSql("select * from FileMaker_Tables")).map(e=>({name:e[0],id:parseInt(e[1]),baseTableName:e[2],fileName:e[3],modCount:parseInt(e[4])})),r=await fmPromise.evaluate("ScriptNames ( get(filename) )"),i=await fmPromise.evaluate("LayoutNames ( get(filename) )"),o=await async function(){const e=await fmPromise.evaluate('While (\n  [\n    i = 0 ;\n    names = LayoutObjectNames ( Get(FileName) ; Get(LayoutName) ) ;\n    count = ValueCount ( names ) ;\n    result = "[]"\n  ] ;\n  i < count;\n  [\n    i = i + 1 ;\n    eachName = GetValue ( names ; i ) ;\n    enclosingObject = GetLayoutObjectAttribute ( eachName ; "enclosingObject" ) ;\n    containedObjects = GetLayoutObjectAttribute ( eachName ; "containedObjects" ) ;\n    containedArray = "[" & Substitute ( Quote ( containedObjects ) ; "\\¶" ; "\\",\\"" ) & "]" ;\n    element = JSONSetElement( "" ;\n      [ "name" ; eachName ; JSONString ] ;\n      [ "objectType" ; GetLayoutObjectAttribute ( eachName ; "objectType" ) ; JSONString ] ;\n      [ "top" ; GetLayoutObjectAttribute ( eachName ; "top" ) ; JSONNumber ] ;\n      [ "left" ; GetLayoutObjectAttribute ( eachName ; "left" ) ; JSONNumber ] ;\n      [ "width" ; GetLayoutObjectAttribute ( eachName ; "width" ) ; JSONNumber ] ;\n      [ "height" ; GetLayoutObjectAttribute ( eachName ; "height" ) ; JSONNumber ] ;\n      [ "rotation" ; GetLayoutObjectAttribute ( eachName ; "rotation" ) ; JSONNumber ] ;\n      [ "source" ; GetLayoutObjectAttribute ( eachName ; "source" ) ; JSONString ] ;\n      [ "enclosingObject" ; enclosingObject ; Case ( IsEmpty ( enclosingObject ) ; JSONNull ; JSONString ) ] ;\n      [ "containedObjects" ; containedArray ; Case (IsEmpty ( containedObjects ) ; JSONNull ; JSONArray ) ] ;\n      [ "hidden" ; GetLayoutObjectAttribute ( eachName ; "isObjectHidden" ) ; JSONBoolean ]\n    ) ;\n    result = Case ( eachName = "<" or eachName = ">" ; result ; JSONSetElement ( result ; i-1 ; element ; JSONRaw ) )\n  ] ;\n  result\n)');return console.log("Layout objects:",e),e}(),l=await async function(){const e=fmPromise.evaluate('While (\n  [\n    i = 0 ;\n    arrayIndex = 0 ;\n    rels = RelationInfo ( Get ( Filename ) ; Get ( LayoutTableName ) ) ;\n    count = valuecount ( rels ) ;\n    result = "[]"\n  ] ;\n  i < count ;\n  [\n    i = i + 1 ;\n    source = GetValue ( Substitute ( GetValue ( rels ; i ) ; ":" ; ¶ ) ; 2 ) ;\n    i = i + 1 ;\n    table = GetValue ( Substitute ( GetValue ( rels ; i ) ; ":" ; ¶ ) ; 2 ) ;\n    i = i + 1 ;\n    options = GetValue ( Substitute ( GetValue ( rels ; i ) ; ":" ; ¶ ) ; 2 ) ;\n    optionsArray = "[" & case ( not isempty ( options ) ; Substitute ( quote ( options ) ; " " ; "\\",\\"" ) ) & "]" ;\n    i = i + 1 ;\n    predicates = While (\n      [\n        predicates = "[]";\n        j = i ;\n        pIndex = 0\n      ] ;\n      not IsEmpty ( getvalue ( rels ; j ) ) ;\n      [\n        predicates = jsonsetelement ( predicates ; pIndex ; getvalue ( rels ; j ) ; JSONString ) ;\n        pIndex = pIndex + 1 ;\n        j = j + 1\n      ] ;\n      let ( i = j ; predicates )\n    ) ;\n    eachElement = JSONSetElement ( "{}" ;\n      [ "file" ; source ; JSONString ] ;\n      [ "table" ; table ; JSONString ] ;\n      [ "options" ; optionsArray ; JSONArray ] ;\n      [ "predicates" ; predicates ; JSONRaw ]\n    ) ;\n    result = JSONSetElement ( result ; arrayIndex ; eachElement ; JSONRaw ) ;\n    arrayIndex = arrayIndex + 1 ;\n    i = i + 1\n  ] ;\n  result\n)\n');return console.log("Relationships:",e),e}(),c=await fmPromise.evaluate("ValueListNames(Get(FileName))"),u=document.createElement("form");u.classList.add("fmPromiseConfiguration");const m=await fmPromise.evaluate("Get(LayoutTableName)");u.append(...Object.entries(e).map(e=>{let[n,u]=e;"string"==typeof u&&(u={type:u});const s=document.createElement("label");if(s.innerText=u.label||function(e){return e?(e.charAt(0).toUpperCase()+e.substr(1)).replace(/([a-z])([A-Z])/g,"$1 $2").replace(/([A-Z])([A-Z][a-z])/g,"$1 $2").replace(/([a-z])([0-9])/gi,"$1 $2").replace(/([0-9])([a-z])/gi,"$1 $2"):""}(n),"field"===u.type){const e=document.createElement("select");e.name=n+".table";const r=document.createElement("select");return r.name=n,r.required=!1!==u.required,e.append(...a.filter(u.tableFilter||(e=>"fmPromiseModule"!==e.name&&"fmPromiseWebViewer"!==e.name)).map(e=>{const t=document.createElement("option");return t.innerText=e.name,t})),e.addEventListener("change",async e=>{for(;r.firstChild;)r.firstChild.remove();r.appendChild(document.createElement("option"));const a=(await fmPromise.executeSql("select * from FileMaker_Fields where TableName=?",e.target.value)).map(e=>({tableName:e[0],name:e[1],type:e[2],id:parseInt(e[3]),class:e[4],reps:parseInt(e[5]),modCount:parseInt(e[6])}));r.append(...a.filter(u.columnFilter||(()=>!0)).map(e=>{const t=document.createElement("option");return t.value=e.tableName+"::"+e.name,t.innerText=e.name,t})),r.value=t[n]||null}),e.value=t[n]||m,e.dispatchEvent(new Event("change")),"number"===u.type?r.addEventListener("change",e=>t[n]=e.target.value.length?parseInt(e.target.value):null):r.addEventListener("change",e=>t[n]=e.target.value),[s,e,r]}if("layout"===u.type){const e=document.createElement("select");return e.name=n,e.required=!1!==u.required,e.appendChild(document.createElement("option")),e.append(...i.split("\r").filter(u.layoutFilter||(e=>0!==e.indexOf("fmPromise"))).map(e=>{let t=document.createElement("option");return t.innerText=e,t})),e.value=t[n]||"",e.addEventListener("change",e=>t[n]=e.target.value),[s,e]}if("layoutObject"===u.type){const e=document.createElement("select");return e.name=n,e.required=!1!==u.required,e.appendChild(document.createElement("option")),e.append(...o.filter(Boolean).filter(u.layoutObjectFilter||(e=>-1===e.name.indexOf("fmPromiseWebViewer_"))).map(e=>{let t=document.createElement("option");return t.innerText=e.name,t})),e.value=t[n]||"",e.addEventListener("change",e=>t[n]=e.target.value),[s,e]}if("relationship"===u.type){const e=document.createElement("select");return e.name=n,e.required=!1!==u.required,e.appendChild(document.createElement("option")),e.append(...l.filter(u.relationshipFilter||(()=>!0)).map(e=>{let t=document.createElement("option");return t.innerText=e.table,t})),e.value=t[n]||"",e.addEventListener("change",e=>t[n]=e.target.value),[s,e]}if("script"===u.type){const e=document.createElement("select");return e.name=n,e.required=!1!==u.required,e.appendChild(document.createElement("option")),e.append(...r.split("\r").filter(u.scriptFilter||(e=>0!==e.indexOf("fmPromise"))).map(e=>{let t=document.createElement("option");return t.innerText=e,t})),e.value=t[n]||"",e.addEventListener("change",e=>t[n]=e.target.value),[s,e]}if("string"===u.type||"text"===u.type||"number"===u.type||"color"===u.type){const e=document.createElement("input");return e.name=n,e.required=!1!==u.required,e.value=t[n]||"",u.type&&(e.type=u.type),u.min&&(e.min=u.min),u.max&&(e.max=u.max),u.step&&(e.step=u.step),u.placeholder&&(e.placeholder=u.placeholder),u.minLength&&(e.minLength=u.minLength),u.maxLength&&(e.maxLength=u.maxLength),e.addEventListener("change",e=>t[n]=e.target.value),[s,e]}if("boolean"===u.type||"checkbox"===u.type){const e=document.createElement("input");return e.name=n,e.type="checkbox",e.checked=!!t[n],e.addEventListener("change",e=>t[n]=e.target.checked),e.selected=!0===t[n],[s,e]}if("valueList"===u.type){const e=document.createElement("select");return e.name=n,e.required=!1!==u.required,e.appendChild(document.createElement("option")),e.append(...c.split("\r").filter(u.valueListFilter||(e=>!0)).map(e=>{let t=document.createElement("option");return t.innerText=e,t})),e.value=t[n]||"",e.addEventListener("change",e=>t[n]=e.target.value),[s,e]}throw"Unsupported config type in "+JSON.stringify(u)}).map(e=>{const t=document.createElement("div");return t.classList.add("config-input"),t.append(...e),t}));const s=document.createElement("button");return s.type="submit",s.innerText="Save",u.append(s),document.body.appendChild(u),new Promise((e,a)=>{u.addEventListener("submit",async a=>{a.preventDefault();try{for(;document.body.firstChild;)document.body.firstChild.remove();for(;n.firstChild;)document.body.appendChild(n.firstChild);e(t)}catch(a){alert("Unable to save configuration: "+(a.message||a))}})})}}}();]]></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[The javascript which generates a configuration form for an installed fmPromise module, when given a schema object indicating which fields / types to prompt for.]]></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['use strict';const fmPromiseConfiguration = function () {	function titleCase(camelCase) {		if (!camelCase) {			return '';		}		const pascalCase = camelCase.charAt(0).toUpperCase() + camelCase.substr(1);		return pascalCase			.replace(/([a-z])([A-Z])/g, '$1 $2')			.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')			.replace(/([a-z])([0-9])/gi, '$1 $2')			.replace(/([0-9])([a-z])/gi, '$1 $2');	}	async function getLayoutObjects() {		const result = await fmPromise.evaluate('While (\n' +			'  [\n' +			'    i = 0 ;\n' +			'    names = LayoutObjectNames ( Get(FileName) ; Get(LayoutName) ) ;\n' +			'    count = ValueCount ( names ) ;\n' +			'    result = "[]"\n' +			'  ] ;\n' +			'  i < count;\n' +			'  [\n' +			'    i = i + 1 ;\n' +			'    eachName = GetValue ( names ; i ) ;\n' +			'    enclosingObject = GetLayoutObjectAttribute ( eachName ; "enclosingObject" ) ;\n' +			'    containedObjects = GetLayoutObjectAttribute ( eachName ; "containedObjects" ) ;\n' +			'    containedArray = "[" & Substitute ( Quote ( containedObjects ) ; "\\¶" ; "\\",\\"" ) & "]" ;\n' +			'    element = JSONSetElement( "" ;\n' +			'      [ "name" ; eachName ; JSONString ] ;\n' +			'      [ "objectType" ; GetLayoutObjectAttribute ( eachName ; "objectType" ) ; JSONString ] ;\n' +			'      [ "top" ; GetLayoutObjectAttribute ( eachName ; "top" ) ; JSONNumber ] ;\n' +			'      [ "left" ; GetLayoutObjectAttribute ( eachName ; "left" ) ; JSONNumber ] ;\n' +			'      [ "width" ; GetLayoutObjectAttribute ( eachName ; "width" ) ; JSONNumber ] ;\n' +			'      [ "height" ; GetLayoutObjectAttribute ( eachName ; "height" ) ; JSONNumber ] ;\n' +			'      [ "rotation" ; GetLayoutObjectAttribute ( eachName ; "rotation" ) ; JSONNumber ] ;\n' +			'      [ "source" ; GetLayoutObjectAttribute ( eachName ; "source" ) ; JSONString ] ;\n' +			'      [ "enclosingObject" ; enclosingObject ; Case ( IsEmpty ( enclosingObject ) ; JSONNull ; JSONString ) ] ;\n' +			'      [ "containedObjects" ; containedArray ; Case (IsEmpty ( containedObjects ) ; JSONNull ; JSONArray ) ] ;\n' +			'      [ "hidden" ; GetLayoutObjectAttribute ( eachName ; "isObjectHidden" ) ; JSONBoolean ]\n' +			'    ) ;\n' +			'    result = Case ( eachName = "<" or eachName = ">" ; result ; JSONSetElement ( result ; i-1 ; element ; JSONRaw ) )\n' +			'  ] ;\n' +			'  result\n' +			')');		console.log('Layout objects:', result);		return result;	}	async function getRelationships() {		const result = fmPromise.evaluate('While (\n' +			'  [\n' +			'    i = 0 ;\n' +			'    arrayIndex = 0 ;\n' +			'    rels = RelationInfo ( Get ( Filename ) ; Get ( LayoutTableName ) ) ;\n' +			'    count = valuecount ( rels ) ;\n' +			'    result = "[]"\n' +			'  ] ;\n' +			'  i < count ;\n' +			'  [\n' +			'    i = i + 1 ;\n' +			'    source = GetValue ( Substitute ( GetValue ( rels ; i ) ; ":" ; ¶ ) ; 2 ) ;\n' +			'    i = i + 1 ;\n' +			'    table = GetValue ( Substitute ( GetValue ( rels ; i ) ; ":" ; ¶ ) ; 2 ) ;\n' +			'    i = i + 1 ;\n' +			'    options = GetValue ( Substitute ( GetValue ( rels ; i ) ; ":" ; ¶ ) ; 2 ) ;\n' +			'    optionsArray = "[" & case ( not isempty ( options ) ; Substitute ( quote ( options ) ; " " ; "\\",\\"" ) ) & "]" ;\n' +			'    i = i + 1 ;\n' +			'    predicates = While (\n' +			'      [\n' +			'        predicates = "[]";\n' +			'        j = i ;\n' +			'        pIndex = 0\n' +			'      ] ;\n' +			'      not IsEmpty ( getvalue ( rels ; j ) ) ;\n' +			'      [\n' +			'        predicates = jsonsetelement ( predicates ; pIndex ; getvalue ( rels ; j ) ; JSONString ) ;\n' +			'        pIndex = pIndex + 1 ;\n' +			'        j = j + 1\n' +			'      ] ;\n' +			'      let ( i = j ; predicates )\n' +			'    ) ;\n' +			'    eachElement = JSONSetElement ( "{}" ;\n' +			'      [ "file" ; source ; JSONString ] ;\n' +			'      [ "table" ; table ; JSONString ] ;\n' +			'      [ "options" ; optionsArray ; JSONArray ] ;\n' +			'      [ "predicates" ; predicates ; JSONRaw ]\n' +			'    ) ;\n' +			'    result = JSONSetElement ( result ; arrayIndex ; eachElement ; JSONRaw ) ;\n' +			'    arrayIndex = arrayIndex + 1 ;\n' +			'    i = i + 1\n' +			'  ] ;\n' +			'  result\n' +			')\n');		console.log('Relationships:', result);		return result;	}	return {		async show(schema, result) {			// move everything out of the body, into a hidden div			const hidden = document.createElement('div');			hidden.style.display = 'none';			while (document.body.firstChild) {				hidden.appendChild(document.body.firstChild)			}			document.body.append(hidden);			// create config form			// noinspection SqlResolve			const allTables = (await fmPromise.executeSql('select * from FileMaker_Tables'))				.map(r => ({name: r[0], id: parseInt(r[1]), baseTableName: r[2], fileName: r[3], modCount: parseInt(r[4])}));			const allScripts = await fmPromise.evaluate('ScriptNames ( get(filename) )');			const allLayouts = await fmPromise.evaluate('LayoutNames ( get(filename) )');			const allLayoutObjects = await getLayoutObjects();			const allRelationships = await getRelationships();			const allValueLists = await fmPromise.evaluate('ValueListNames(Get(FileName))')			// build configuration UI			const ui = document.createElement('form');			ui.classList.add('fmPromiseConfiguration');			const defaultTableName = await fmPromise.evaluate('Get(LayoutTableName)');			ui.append(...Object.entries(schema)				.map(entry => {					let [name, definition] = entry;					if (typeof definition === 'string') {						definition = {type: definition}; // shorthand					}					const label = document.createElement('label');					label.innerText = definition.label || titleCase(name);					if (definition.type === 'field') {						const tableSelect = document.createElement('select');						tableSelect.name = name + '.table';						const columnSelect = document.createElement('select');						columnSelect.name = name;						columnSelect.required = definition.required !== false;						tableSelect.append(...allTables							.filter(definition.tableFilter || (f => f.name !== 'fmPromiseModule' && f.name !== 'fmPromiseWebViewer'))							.map(t => {								const option = document.createElement('option');								option.innerText = t.name;								return option;							})						)						tableSelect.addEventListener('change', async e => {							while (columnSelect.firstChild) {								columnSelect.firstChild.remove();							}							columnSelect.appendChild(document.createElement('option'));							// noinspection SqlResolve							const columnsInTable = (await fmPromise.executeSql('select * from FileMaker_Fields where TableName=?', e.target.value))								.map(r => ({tableName: r[0], name: r[1], type: r[2], id: parseInt(r[3]), class: r[4], reps: parseInt(r[5]), modCount: parseInt(r[6])}));							columnSelect.append(...columnsInTable								.filter(definition.columnFilter || (() => true))								.map(c => {									const option = document.createElement('option');									option.value = c.tableName + '::' + c.name;									option.innerText = c.name;									return option;								}));							columnSelect.value = result[name] || null;						})						tableSelect.value = result[name] || defaultTableName;						tableSelect.dispatchEvent(new Event('change'));						if (definition.type === 'number') {							columnSelect.addEventListener('change', e => result[name] = e.target.value.length ? parseInt(e.target.value) : null)						} else {							columnSelect.addEventListener('change', e => result[name] = e.target.value)						}						return [label, tableSelect, columnSelect];					} else if (definition.type === 'layout') {						const layoutSelect = document.createElement('select');						layoutSelect.name = name;						layoutSelect.required = definition.required !== false						layoutSelect.appendChild(document.createElement('option')); // empty option						layoutSelect.append(...allLayouts							.split('\r')							.filter(definition.layoutFilter || (f => f.indexOf('fmPromise') !== 0))							.map(s => {								let o = document.createElement('option');								o.innerText = s;								return o;							}))						layoutSelect.value = result[name] || '';						layoutSelect.addEventListener('change', e => result[name] = e.target.value)						return [label, layoutSelect]					} else if (definition.type === 'layoutObject') {						const namedObjectSelect = document.createElement('select');						namedObjectSelect.name = name;						namedObjectSelect.required = definition.required !== false						namedObjectSelect.appendChild(document.createElement('option')); // empty option						namedObjectSelect.append(...allLayoutObjects								.filter(Boolean)								.filter(definition.layoutObjectFilter || (l => l.name.indexOf('fmPromiseWebViewer_') === -1))								.map(layoutObject => {									let o = document.createElement('option');									o.innerText = layoutObject.name;									return o;								}));						namedObjectSelect.value = result[name] || '';						namedObjectSelect.addEventListener('change', e => result[name] = e.target.value)						return [label, namedObjectSelect]					} else if (definition.type === 'relationship') {						const relSelect = document.createElement('select');						relSelect.name = name;						relSelect.required = definition.required !== false						relSelect.appendChild(document.createElement('option')); // empty option						relSelect.append(...allRelationships							.filter(definition.relationshipFilter || (() => true))							.map(relationship => {								let o = document.createElement('option');								o.innerText = relationship.table;								return o;							}));						relSelect.value = result[name] || '';						relSelect.addEventListener('change', e => result[name] = e.target.value)						return [label, relSelect]					} else if (definition.type === 'script') {						const scriptSelect = document.createElement('select');						scriptSelect.name = name;						scriptSelect.required = definition.required !== false						scriptSelect.appendChild(document.createElement('option')); // empty option						scriptSelect.append(...allScripts							.split('\r')							.filter(definition.scriptFilter || (f => f.indexOf('fmPromise') !== 0))							.map(s => {								let o = document.createElement('option');								o.innerText = s;								return o;							}))						scriptSelect.value = result[name] || '';						scriptSelect.addEventListener('change', e => result[name] = e.target.value)						return [label, scriptSelect]					} else if (definition.type === 'string' || definition.type === 'text' || definition.type === 'number' || definition.type === 'color') {						const input = document.createElement('input');						input.name = name;						input.required = definition.required !== false;						input.value = result[name] || '';						if ( definition.type) input.type = definition.type;						if ( definition.min ) input.min = definition.min;						if ( definition.max ) input.max = definition.max;						if ( definition.step ) input.step = definition.step;						if ( definition.placeholder ) input.placeholder = definition.placeholder;						if ( definition.minLength ) input.minLength = definition.minLength;						if ( definition.maxLength ) input.maxLength = definition.maxLength;						input.addEventListener('change', e => result[name] = e.target.value)						return [label, input]					} else if (definition.type === 'boolean' || definition.type === 'checkbox') {						const input = document.createElement('input');						input.name = name;						input.type = 'checkbox';						input.checked = !!result[name];						input.addEventListener('change', e => result[name] = e.target.checked)						input.selected = result[name] === true;						return [label, input]					} else if (definition.type === 'valueList') {						const vlSelect = document.createElement('select');						vlSelect.name = name;						vlSelect.required = definition.required !== false						vlSelect.appendChild(document.createElement('option')); // empty option						vlSelect.append(...allValueLists							.split('\r')							.filter(definition.valueListFilter || (f => true))							.map(s => {								let o = document.createElement('option');								o.innerText = s;								return o;							}))						vlSelect.value = result[name] || '';						vlSelect.addEventListener('change', e => result[name] = e.target.value)						return [label, vlSelect]					} else {						throw 'Unsupported config type in ' + JSON.stringify(definition);					}				})				.map(components => {					const div = document.createElement('div');					div.classList.add('config-input')					div.append(...components);					return div;				}))			const button = document.createElement('button');			button.type = 'submit';			button.innerText = 'Save';			ui.append(button);			document.body.appendChild(ui);			return new Promise((resolve, reject) => {				ui.addEventListener('submit', async e => {					e.preventDefault(); // don't submit the form, which would reload the page					try {						// clear the UI from body						while (document.body.firstChild) {							document.body.firstChild.remove();						}						// move hidden stuff back into the body						while (hidden.firstChild) {							document.body.appendChild(hidden.firstChild);						}						resolve(result)					} catch (e) {						alert('Unable to save configuration: ' + (e.message || e));					}				});			})		}	}}();]]></Data>
							</StyledText>
						</Cell>
					</Row>
					<Row membercount="11" id="8">
						<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[7AE9027B-835C-407E-8A73-91F1C6D87049]]></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[09-12-2021 06:55:43 PM]]></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[configuration-example.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">	<script>"use strict";const fmPromise=function(){let e=0;const t={},r={},n=new Promise((e,t)=>{let r=100;const n=window.setInterval(()=>{window.FileMaker?(window.clearInterval(n),e(window.FileMaker)):r--||(window.clearInterval(n),t("No window.FileMaker object was loaded after polling timeout."))},10)});function o(e){Object.assign(this,e),this.message=e.message||"Unknown error",this.name=this.constructor.name}return o.prototype=new Error,o.prototype.toString=function(){return this.message+(this.code?" ("+this.code+")":"")},{webViewerName:document.$FMP_WEB_VIEWER_NAME||new URLSearchParams(window.location.search).get("webViewerName")||"fmPromiseWebViewer",async configuration(e){let t=Object.entries(e).reduce((e,t)=>(e[t[0]]=t[1].value,e),{});await n;const r=await this.executeSql("select configuration from fmPromiseWebViewer where id=?",this.webViewerName);if(r.length&&r[0][0])try{const n=JSON.parse(r[0][0]);let o=Object.entries(e).filter(e=>{let[t,r]=e;return"string"==typeof r&&(r={type:r}),!1!==r.required&&"boolean"!==r.type&&!n[t]});if(0===o.length)return n;console.warn("Config is out of date",r,"missing key(s)",o),Object.assign(t,n)}catch(e){console.error("Could not parse",r,e)}await fmPromise.loadModule("fm-promise-config-form.js");const o=await fmPromiseConfiguration.show(e,t);return fmPromise.performScript("fmPromise.configure",o)},async loadModule(e){if(!r[e])return r[e]=!0,new Promise(async(t,r)=>{const n=document.createElement("script");if(n.type="text/javascript",0===e.indexOf("http"))n.src=src,n.onload=function(){t(src)};else{const s=await this.executeSql("select coalesce(sourceMinified, source) from fmPromiseModule where filename=?",e);s.length||r(new o({message:"Unable to locate fmPromise module "+e})),n.text=s[0][0],t(s)}document.head.appendChild(n)})},performScript(r,o=null,s={runningScript:0,alwaysReturnString:!1}){o&&"string"!=typeof o&&(o=JSON.stringify(o));const i=s.runningScript||0,a=++e;return n.then(e=>new Promise((n,s)=>{const c=this.webViewerName,l=JSON.stringify({scriptName:r,promiseId:a,webViewerName:c});t[a]={resolve:n,reject:s},console.info(a+' Performing script "'+r+'" with param '+(o?o.length:0)+" bytes");const m=l+"\n"+o;0===i?e.PerformScript("fmPromise",m):e.PerformScriptWithOption("fmPromise",m,i)})).then(e=>{if(!s.alwaysReturnString&&e&&"{"===e[0]||"["===e[0])try{e=JSON.parse(e)}catch(t){console.warn("Unable to parse JSON result ",e,t)}return e})},evaluate(e,t={},r={}){const n="Let(["+Object.entries(t||{}).map(e=>e[0]+"="+JSON.stringify(e[1])).join(";")+"] ; "+e+")";return this.performScript("fmPromise.evaluate",n,r)},executeFileMakerDataAPI(e){return this.performScript("fmPromise.executeFileMakerDataAPI",e).then(e=>{if(e&&e.messages&&e.messages.length){if("0"!==e.messages[0].code)throw new o(e.messages[0]);return e.response}throw new o({code:-1,message:"Empty data API response"})})},async executeFileMakerDataAPIRecords(e){return(await this.executeFileMakerDataAPI(e)).data.map(e=>{const t=e.fieldData;return t.recordId=e.recordId,t.modId=e.modId,Object.assign(t,e.portalData),t})},executeSql(e,...t){const r=t.map(e=>" ; "+JSON.stringify(e)).join(""),n="|"+Math.random()+"|",o="~"+Math.random()+"~";return this.evaluate("ExecuteSQL("+JSON.stringify(e)+' ; "'+n+'" ; "'+o+'"'+r+" )",null,{alwaysReturnString:!0}).then(e=>0===e.length?[]:e.split(o).map(e=>e.split(n)))},executeSqlTemplate(e,...t){if(!Array.isArray(e)||t.length!==e.length-1)throw new o({code:-1,message:"Invalid template literal for executeSqlTemplate"});return this.executeSql(e.join("?"),t)},insertFromUrl(e,t=""){return this.performScript("fmPromise.insertFromURL",{url:e,curlOptions:t})},setFieldByName(e,t){return this.performScript("fmPromise.setFieldByName",{fmFieldNameToSet:e,value:t})},showCustomDialog(e,t,r="OK",n="",o=""){return this.performScript("fmPromise.showCustomDialog",{title:e,body:t,btn1:r,btn2:n,btn3:o}).then(function(e){return parseInt(e)})},_resolve:(e,r)=>(console.info(e+" Resolve with "+r.length+" chars"),t[e].resolve(r),delete t[e]),_reject(e,r){let n;try{n=JSON.parse(r)}catch(e){console.warn("Unable to parse error response as JSON",r,e),n={message:r}}return console.warn(e+" Reject",r),t[e].reject(new o(n)),delete t[e]}}}();</script>	<script>		'use strict';		window.onload = async function() {			await fmPromise.loadModule('fm-promise-config-form.js');			const config = await fmPromiseConfiguration.show({				simpleText:'text',				complexText: {					type:'text',					required:false,					minLength:5,					maxLength:10,					placeholder:'5-10 chars'				},				script:{type:'script', required:false},				field:{type:'field', required:false},				boolean:{type: 'boolean'},				number:{type: 'number', min:1, max:100, placeholder:'1-100', required:false},				color:{type: 'color', required:false},				layout:{type: 'layout', required:false},				layoutObject:{type: 'layoutObject', required:false},				relationship:{type: 'relationship', required:false},				valueList:{type: 'valueList', required:false},				fieldFilteredNonGlobal: {					label:'Field Filtered (no globals)',					type:'field',					required: false,					tableFilter: f => f.name !== 'fmPromiseModule',					columnFilter: c => c.type.indexOf('global') === -1 // exclude globals				},			}, {})			document.getElementById('output').innerText = JSON.stringify(config, null, '\t');		}	</script>	<style>html,body{height:100%;margin:0;padding:0}p,li,div,h1,h2,h3,h4,h5{font-family:Arial,Helvetica,sans-serif;color:#707070}</style></head><body><p>Your custom fmPromise module can pass a configuration block to <code>fmPromise.config({myField:{type:'text'}})</code> and get back a Promise which resolves to a user-entered configuration object.</p><p>This configuration object will be persisted for that web viewer.</p><p>The configuration object you just filled out is below:</p><pre id="output"></pre></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[Showcase all available configuration types. Note: fmPromise layouts and tables are excluded from the configuration options.]]></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 src="fm-promise.js"></script>
	<script>
		'use strict';

		window.onload = async function() {
			await fmPromise.loadModule('fm-promise-config-form.js');
			const config = await fmPromiseConfiguration.show({
				simpleText:'text',
				complexText: {
					type:'text',
					required:false,
					minLength:5,
					maxLength:10,
					placeholder:'5-10 chars'
				},
				script:{type:'script', required:false},
				field:{type:'field', required:false},
				boolean:{type: 'boolean'},
				number:{type: 'number', min:1, max:100, placeholder:'1-100', required:false},
				color:{type: 'color', required:false},
				layout:{type: 'layout', required:false},
				layoutObject:{type: 'layoutObject', required:false},
				relationship:{type: 'relationship', required:false},
				valueList:{type: 'valueList', required:false},
				fieldFilteredNonGlobal: {
					label:'Field Filtered (no globals)',
					type:'field',
					required: false,
					tableFilter: f => f.name !== 'fmPromiseModule',
					columnFilter: c => c.type.indexOf('global') === -1 // exclude globals
				},

			}, {})
			document.getElementById('output').innerText = JSON.stringify(config, null, '\t');
		}
	</script>
	<style>
		html, body {height: 100%;margin: 0;padding: 0;}
		p, li, div, h1, h2, h3, h4, h5 {font-family: Arial, Helvetica, sans-serif;color: #707070;}
	</style>
</head>
<body>
<p>Your custom fmPromise module can pass a configuration block to <code>fmPromise.config({myField:{type:'text'}})</code> and get back a Promise which resolves to a user-entered configuration object.</p>
<p>This configuration object will be persisted for that web viewer.</p>
<p>The configuration object you just filled out is below:</p>
<pre id="output"></pre>
</body>
</html>
]]></Data>
							</StyledText>
						</Cell>
					</Row>
				</RowList>
			</Records>
		</AddAction>
	</Data>
</FMAdd_on>
