UNPKG

@360works/fmpromise

Version:
80 lines 246 kB
��<?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[0