Related Record Summary Grid

Summarize related records, linked items, and operational context in a compact grid for review pages.

Custom CodeUI Design & LayoutsYeeflow
custom coderelated recordssummary gridoperationstsx

Explore the key screens

Explore the key screens and structure included in this template.

Abstract illustration for Related Record Summary Grid, showing related record visibility and operational context.

What this template helps teams build

Overview

The Related Record Summary Grid template is a reusable custom code control concept for Yeeflow builders. It helps teams add a focused interface pattern for related record visibility and operational context while keeping the asset in review before public launch.

Key capabilities

  • Supports operational work scenarios
  • Designed for use in Record detail, Application page, Dashboard
  • Includes TSX source, user guidance, and example configuration
  • Prepared as a preview asset for technical review and future public documentation

Recommended use cases

  • related record visibility and operational context
  • Operational Work template library planning
  • Internal builder education and implementation review

Governance notes

This record is created in review status for editorial and product validation. It should not be positioned as launchable until implementation, security, runtime, and documentation review are complete.

Build patterns behind this template

Use this template as a reference while reviewing the Custom Code developer guide. Learn how the required export structure, input parameters, and rendering patterns fit together.

Code preview

Download source file
import React from 'react';

const TEMPLATE_KIND = 'related-record-summary-grid';
const TEMPLATE_TITLE = 'Related Record Summary Grid';
const TEMPLATE_DESC = 'Related Record Summary Grid - reusable Yeeflow component for showing related records inline as cards or table rows.';
const WRITABLE_TARGETS = [];

export class CodeInApplication implements CodeInComp {
  description() { return TEMPLATE_DESC; }
  requiredFields(params?: CodeInParams) { return []; }
  inputParameters(): InputParameter[] { return [
      {"id": "targetListId", "name": "Target List ID", "type": "variable", "desc": "Related data list ID.", "example": "203..."},
      {"id": "relationField", "name": "Relation Field", "type": "variable", "desc": "Field used to match relation.", "example": "RequestId"},
      {"id": "relationValue", "name": "Relation Value", "type": "variable", "desc": "Current relation value.", "example": "{{ListDataID}}"},
      {"id": "displayedFieldsJson", "name": "Displayed Fields JSON", "type": "variable", "desc": "JSON/comma list of fields to display.", "example": "[\"Title\",\"Status\"]"},
      {"id": "displayType", "name": "Display Type", "type": "string", "desc": "card or table.", "example": "card"},
      {"id": "titleField", "name": "Title Field", "type": "variable", "desc": "Primary field.", "example": "Title"},
      {"id": "statusField", "name": "Status Field", "type": "variable", "desc": "Status field.", "example": "Status"},
      {"id": "titleText", "name": "Title Text", "type": "string", "desc": "Panel title.", "example": "Related Records"},
      {"id": "subtitleText", "name": "Subtitle Text", "type": "string", "desc": "Panel subtitle.", "example": "Linked records"},
      {"id": "maxItems", "name": "Max Items", "type": "string", "desc": "Max records.", "example": "6"},
      {"id": "emptyText", "name": "Empty Text", "type": "string", "desc": "Empty text.", "example": "No related records"},
      {"id": "filterExpression", "name": "Filter Expression", "type": "variable", "desc": "Optional filter.", "example": ""}
    ]; }
  render(context: CodeInContext, fieldsValues: any, readonly: boolean) { return <ReusableTemplatePanel context={context} fieldsValues={fieldsValues} readonly={readonly} params={(context && context.params) || {}} configuredOutputTargets={(this as any).configuredTargetMap || {}} />; }
}

class ReusableTemplatePanel extends React.Component<any, any> {
  private mountedFlag: boolean;
  constructor(props: any) {
    super(props);
    this.mountedFlag = false;
    this.state = this.createInitialState(props);
  }
  componentDidMount() { this.mountedFlag = true; if (this.needsQuery()) { this.loadData(); } }
  componentDidUpdate(previousProps: any) { if (this.getConfigKey(previousProps) !== this.getConfigKey(this.props)) { this.setState(this.createInitialState(this.props), () => { if (this.needsQuery()) { this.loadData(); } }); } }
  componentWillUnmount() { this.mountedFlag = false; }
  createInitialState(props: any) { const config=this.getConfigFromProps(props); return { loading:false,errorText:'',rows:[],selected:[],tags:this.readInitialArray(config),checklist:this.readInitialChecklist(config),parentValue:this.toString(this.readTarget(config.parentTarget)),childValue:this.toString(this.readTarget(config.childTarget)),textValue:'',validationMessage:'' }; }
  needsQuery() { return ['approval-timeline','related-record-summary-grid','hierarchical-selector','dependent-selector','activity-timeline','exception-alert-panel'].indexOf(TEMPLATE_KIND)!==-1; }
  getConfig() { return this.getConfigFromProps(this.props); }
  getConfigFromProps(props: any) { const params=(props&&props.params)||{}; return {
    titleText:this.toString(params.titleText)||TEMPLATE_TITLE,
    subtitleText:this.toString(params.subtitleText)||'Related records and operational context.',
    emptyText:this.toString(params.emptyText)||'No records to display.',
    dataListId:this.toString(params.dataListId||params.sourceListId||params.targetListId),
    recordIdValue:this.toString(params.recordIdValue),
    recordIdField:this.toString(params.recordIdField),
    relationField:this.toString(params.relationField),
    relationValue:this.toString(params.relationValue),
    displayedFields:this.parseStringArray(params.displayedFieldsJson||params.displayedFields),
    eventDateField:this.toString(params.eventDateField||params.dateField||params.sortField),
    actorField:this.toString(params.actorField),
    titleField:this.toString(params.titleField||params.labelField||params.eventTitleField),
    statusField:this.toString(params.statusField||params.decisionField||params.severityField),
    commentFieldName:this.toString(params.commentField||params.descriptionField),
    idField:this.toString(params.idField)||'ListDataID',
    parentField:this.toString(params.parentField),
    childField:this.toString(params.childField),
    labelField:this.toString(params.labelField||params.titleField)||'Title',
    valueField:this.toString(params.valueField)||'ListDataID',
    filterExpression:this.resolveParameterValue(params.filterExpression),
    maxItems:Math.max(1,this.toNumber(params.maxItems||params.maxRecords||params.maxResults,8)),
    displayType:this.toString(params.displayType)||'card',
    saveMode:this.toString(params.saveMode)||'json',
    delimiter:this.toString(params.delimiter)||',',
    placeholderText:this.toString(params.placeholderText)||'Type and press Enter',
    allowDuplicates:this.toBoolean(params.allowDuplicates,false),
    maxTags:Math.max(1,this.toNumber(params.maxTags||params.maxItems,20)),
    items:this.parseChecklistItems(params.itemsJson),
    requireAllChecked:this.toBoolean(params.requireAllChecked,true),
    showProgress:this.toBoolean(params.showProgress,true),
    multiSelect:this.toBoolean(params.multiSelect,false),
    rules:this.parseRules(params.rulesJson),
    showSeverity:this.toBoolean(params.showSeverity,true),
    saveTarget:this.resolveWriteTarget('saveToField',params.saveToField),
    selectedItemsTarget:this.resolveWriteTarget('selectedItemsField',params.selectedItemsField),
    parentTarget:this.resolveWriteTarget('parentSaveToField',params.parentSaveToField),
    childTarget:this.resolveWriteTarget('childSaveToField',params.childSaveToField),
  }; }
  getConfigKey(props:any) { try { return JSON.stringify((props&&props.params)||{}); } catch(error) { return String(new Date().getTime()); } }
  loadData() { const config=this.getConfig(); if(!config.dataListId){return;} const client=this.props.context&&this.props.context.modules&&this.props.context.modules.yeeSDKClient; if(!client||!client.lists||typeof client.lists.queryItems!=='function'){this.setState({errorText:'Data list service is not available'});return;} const fields=[config.recordIdField,config.relationField,config.eventDateField,config.actorField,config.titleField,config.statusField,config.commentFieldName,config.idField,config.parentField,config.childField,config.labelField,config.valueField].concat(config.displayedFields).filter(Boolean); this.setState({loading:true,errorText:''}); this.queryList(client,config.dataListId,fields,Math.max(50,config.maxItems*10),config.filterExpression).then((rows)=>{if(this.mountedFlag){this.setState({loading:false,rows:this.filterRows(rows,config)});}}).catch(()=>{if(this.mountedFlag){this.setState({loading:false,errorText:'Unable to load records'});}}); }
  filterRows(rows:any[],config:any) { let output=rows||[]; if(config.relationField&&config.relationValue){output=output.filter((r)=>this.toString(this.readField(r,config.relationField))===config.relationValue);} if(config.recordIdField&&config.recordIdValue){output=output.filter((r)=>this.toString(this.readField(r,config.recordIdField))===config.recordIdValue);} if(config.eventDateField){output=output.slice().sort((a,b)=>{const ad=this.parseDate(this.readField(a,config.eventDateField));const bd=this.parseDate(this.readField(b,config.eventDateField));return (bd?bd.getTime():0)-(ad?ad.getTime():0);});} return output.slice(0,config.maxItems); }
  readInitialArray(config:any) { const raw=this.readTarget(config.saveTarget); const parsed=this.safeParseJson(raw); if(Array.isArray(parsed)){return parsed.map((x)=>this.toString(x)).filter(Boolean);} return this.toString(raw).split(config.delimiter||',').map((x)=>x.trim()).filter(Boolean); }
  readInitialChecklist(config:any) { const raw=this.readTarget(config.saveTarget); const parsed=this.safeParseJson(raw); const map:any={}; if(Array.isArray(parsed)){parsed.forEach((x:any)=>{if(x&&typeof x==='object'){map[this.toString(x.id||x.value||x.label)]=x.checked===true;}});} return map; }
  parseStringArray(value:any) { const parsed=this.safeParseJson(value); if(Array.isArray(parsed)){return parsed.map((x)=>this.toString(x)).filter(Boolean);} return this.toString(value).split(',').map((x)=>x.trim()).filter(Boolean); }
  parseChecklistItems(value:any) { const parsed=this.safeParseJson(value); const src=Array.isArray(parsed)?parsed:[]; return src.map((item:any,index:number)=>{ if(typeof item==='string'){return {id:item,label:item,required:true};} return {id:this.toString(item.id||item.value||item.key)||'item-'+index,label:this.toString(item.label||item.title||item.name)||('Item '+(index+1)),required:this.toBoolean(item.required,true),helper:this.toString(item.helper||item.description)}; }); }
  parseRules(value:any) { const parsed=this.safeParseJson(value); return Array.isArray(parsed)?parsed:[]; }
  persistTags(tags:string[],config:any) { const clean=config.allowDuplicates?tags:this.unique(tags); const value=config.saveMode==='delimiter'?clean.join(config.delimiter):JSON.stringify(clean); this.writeTarget(config.saveTarget,value); this.setState({tags:clean,textValue:''}); }
  persistChecklist(map:any,config:any) { const value=config.items.map((item:any)=>({id:item.id,label:item.label,checked:map[item.id]===true,required:item.required===true})); this.writeTarget(config.saveTarget,JSON.stringify(value)); const required=config.items.filter((x:any)=>x.required!==false); const done=required.filter((x:any)=>map[x.id]===true).length; this.setState({checklist:map,validationMessage:config.requireAllChecked&&done<required.length?'Complete all required checklist items before submitting.':''}); }
  unique(values:string[]) { const seen:any={}; const out:string[]=[]; values.forEach((v)=>{const s=this.toString(v); const k=s.toLowerCase(); if(s&&!seen[k]){seen[k]=true;out.push(s);}}); return out; }

  resolveParameterValue(value: any): any {
    if (value === undefined || value === null) { return ''; }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || Array.isArray(value)) { return value; }
    if (typeof value === 'object') {
      const preferred = this.firstDefined([value.value,value.Value,value.data,value.Data,value.result,value.Result,value.key,value.Key,value.id,value.Id,value.fieldId,value.FieldId,value.fieldName,value.FieldName,value.label,value.Label,value.name,value.Name]);
      if (preferred !== undefined) { return this.resolveParameterValue(preferred); }
    }
    return value;
  }
  toString(value: any): string {
    const resolved = this.resolveParameterValue(value);
    if (resolved === undefined || resolved === null) { return ''; }
    if (typeof resolved === 'string' || typeof resolved === 'number' || typeof resolved === 'boolean') { return String(resolved).trim(); }
    if (Array.isArray(resolved)) { return resolved.map((item) => this.toString(item)).filter(Boolean).join(', '); }
    if (typeof resolved === 'object') {
      const preferred = this.firstDefined([resolved.value,resolved.Value,resolved.text,resolved.Text,resolved.display,resolved.Display,resolved.label,resolved.Label,resolved.name,resolved.Name,resolved.title,resolved.Title,resolved.key,resolved.Key]);
      if (preferred !== undefined) { return this.toString(preferred); }
      try { return JSON.stringify(resolved); } catch (error) { return ''; }
    }
    return '';
  }
  toBoolean(value: any, fallback: boolean): boolean {
    const resolved = this.resolveParameterValue(value);
    if (resolved === undefined || resolved === null || resolved === '') { return fallback; }
    if (typeof resolved === 'boolean') { return resolved; }
    if (typeof resolved === 'number') { return resolved !== 0; }
    const text = this.toString(resolved).toLowerCase();
    if (['true','1','yes','y','on'].indexOf(text) !== -1) { return true; }
    if (['false','0','no','n','off'].indexOf(text) !== -1) { return false; }
    return fallback;
  }
  toNumber(value: any, fallback: number): number { const parsed = Number(this.toString(value).replace(/,/g,'')); return isNaN(parsed) ? fallback : parsed; }
  safeParseJson(value: any): any { if (Array.isArray(value) || (value && typeof value === 'object')) { return value; } const text=this.toString(value); if(!text){return null;} try{return JSON.parse(text);}catch(error){return null;} }
  firstDefined(values: any[]) { for (let i=0;i<values.length;i+=1){ if(values[i]!==undefined && values[i]!==null){ return values[i]; } } return undefined; }
  readObjectValue(source: any, key: string): any { if(!source || typeof source !== 'object' || !key){return undefined;} if(source[key]!==undefined){return source[key];} const wanted=key.toLowerCase(); const keys=Object.keys(source); for(let i=0;i<keys.length;i+=1){ if(keys[i].toLowerCase()===wanted){ return source[keys[i]]; } } return undefined; }
  normalizeCell(value: any): any { if(value===undefined||value===null){return value;} if(typeof value==='string'||typeof value==='number'||typeof value==='boolean'){return value;} if(Array.isArray(value)){return value.map((item)=>this.toString(this.normalizeCell(item))).filter(Boolean).join(', ');} if(typeof value==='object'){ return this.firstDefined([value.value,value.Value,value.fieldValue,value.FieldValue,value.dataValue,value.DataValue,value.valueText,value.ValueText,value.text,value.Text,value.display,value.Display,value.label,value.Label,value.name,value.Name,value.title,value.Title]); } return value; }
  readField(row: any, field: string): any { if(!row||!field){return undefined;} const direct=this.readObjectValue(row,field); if(direct!==undefined){return this.normalizeCell(direct);} const containers=[row.values,row.Values,row.fields,row.Fields,row.data,row.Data,row.item,row.Item]; for(let i=0;i<containers.length;i+=1){ const v=this.readObjectValue(containers[i],field); if(v!==undefined){return this.normalizeCell(v);} } const arrays=[row.fieldValues,row.FieldValues,row.fieldsValues,row.FieldsValues]; for(let g=0;g<arrays.length;g+=1){ const arr=arrays[g]; if(Array.isArray(arr)){ for(let i=0;i<arr.length;i+=1){ const e=arr[i]||{}; const k=this.toString(this.firstDefined([e.fieldId,e.FieldId,e.fieldID,e.FieldID,e.fieldCode,e.FieldCode,e.name,e.Name,e.key,e.Key])); if(k && k.toLowerCase()===field.toLowerCase()){ return this.normalizeCell(this.firstDefined([e.value,e.Value,e.fieldValue,e.FieldValue,e.dataValue,e.DataValue,e.text,e.Text])); } } } } return undefined; }
  extractRows(response: any): any[] { if(!response){return [];} if(Array.isArray(response)){return response;} const candidates=[response.items,response.Items,response.data,response.Data,response.rows,response.Rows,response.records,response.Records,response.list,response.List,response.result,response.Result,response.value,response.Value]; for(let i=0;i<candidates.length;i+=1){ if(Array.isArray(candidates[i])){return candidates[i];} if(candidates[i]&&typeof candidates[i]==='object'){ const nested=this.extractRows(candidates[i]); if(nested.length>0){return nested;} } } return []; }
  queryList(client: any, listId: string, fields: string[], pageSize: number, filter: any): Promise<any[]> { const selected=['ListDataID','ListDataId'].concat(fields||[]); const parsed=this.safeParseJson(filter); const filterValue=parsed!==null&&parsed!==undefined?parsed:filter; const payload:any={listId:listId,dataListId:listId,fields:selected,fieldIds:selected,selectedFields:selected,pageIndex:1,pageNo:1,pageSize:pageSize}; if(filterValue!==undefined&&filterValue!==null&&this.toString(filterValue)!==''){payload.filters=filterValue;payload.filter=filterValue;payload.where=filterValue;payload.filterExpression=filterValue;} const payload2:any={}; Object.keys(payload).forEach((k)=>{payload2[k]=payload[k];}); delete payload2.listId; delete payload2.dataListId; const attempts=[()=>client.lists.queryItems(payload),()=>client.lists.queryItems(listId,payload2),()=>client.lists.queryItems(listId,payload),()=>client.lists.queryItems(listId,payload2,{})]; const run=(index:number):Promise<any[]>=>{ if(index>=attempts.length){return Promise.resolve([]);} let result:any; try{result=attempts[index]();}catch(error){return run(index+1);} return Promise.resolve(result).then((response)=>{const rows=this.extractRows(response); if(rows.length===0&&index<attempts.length-1){return run(index+1);} return rows;}).catch(()=>run(index+1)); }; return run(0); }
  parseDate(value: any): Date | null { const cell=this.normalizeCell(value); if(cell===undefined||cell===null||cell===''){return null;} if(cell instanceof Date && !isNaN(cell.getTime())){return cell;} if(typeof cell==='number'){ if(cell>100000000000){const d=new Date(cell);return isNaN(d.getTime())?null:d;} if(cell>1000000000){const d=new Date(cell*1000);return isNaN(d.getTime())?null:d;} if(cell>20000&&cell<80000){const d=new Date(Math.round((cell-25569)*86400*1000));return isNaN(d.getTime())?null:d;} } const text=this.toString(cell); const iso=/^(\d{4})-(\d{1,2})-(\d{1,2})/.exec(text); if(iso){return new Date(Number(iso[1]),Number(iso[2])-1,Number(iso[3]));} const parsed=new Date(text); return isNaN(parsed.getTime())?null:parsed; }
  formatDate(value: any): string { const d=this.parseDate(value); if(!d){return this.toString(value);} return d.getFullYear()+'-'+this.pad(d.getMonth()+1)+'-'+this.pad(d.getDate()); }
  pad(value:number){return value<10?'0'+value:String(value);}



  resolveWriteTarget(paramName: string, rawValue: any): any { const configured=(this.props&&this.props.configuredOutputTargets)||{}; const candidates=this.mergeCandidates(this.collectTargets(configured[paramName]),this.collectTargets(rawValue)).filter((x)=>this.isLikelyTarget(x)); return {paramName:paramName,candidates:candidates,displayValue:candidates[0]||this.toString(rawValue)}; }
  collectTargets(rawValue:any): string[]{ const out:string[]=[]; const seen:any={}; const add=(v:any)=>{const s=this.toString(v); if(s&&!seen[s]){seen[s]=true;out.push(s);}}; const visit=(v:any,d:number)=>{ if(v===undefined||v===null||d>4){return;} if(typeof v==='string'||typeof v==='number'||typeof v==='boolean'){add(v);return;} if(Array.isArray(v)){v.forEach((x)=>visit(x,d+1));return;} if(typeof v!=='object'){return;} ['fieldId','FieldId','fieldName','FieldName','variableId','VariableId','variableName','VariableName','tempVariableId','TempVariableId','tempVariableName','TempVariableName','id','Id','key','Key','name','Name','code','Code','label','Label','title','Title','path','Path','value','Value'].forEach((k)=>{if(v[k]!==undefined&&v[k]!==null){add(v[k]);}}); ['target','Target','binding','Binding','field','Field','variable','Variable','tempVariable','TempVariable','data','Data','meta','Meta','metadata','Metadata'].forEach((k)=>{if(v[k]!==undefined&&v[k]!==null){visit(v[k],d+1);}}); }; visit(rawValue,0); return out; }
  mergeCandidates(a:string[],b:string[]){const out:string[]=[];const seen:any={};(a||[]).concat(b||[]).forEach((x)=>{const s=this.toString(x);if(s&&!seen[s]){seen[s]=true;out.push(s);}});return out;}
  isLikelyTarget(value:any){const s=this.toString(value); if(!s||s==='[]'||s==='{}'){return false;} if(s.charAt(0)==='['||s.charAt(0)==='{'){return false;} if(s.indexOf('\":')!==-1||s.indexOf('\",\"')!==-1){return false;} return s.length<=160;}
  writeTarget(target:any,value:any){const candidates=target&&target.candidates?target.candidates:[]; for(let i=0;i<candidates.length;i+=1){ if(this.writeValue(candidates[i],value)){return true;} } return false;}
  writeValue(name:string,value:any){const context=this.props.context||{}; const fieldsValues=this.props.fieldsValues||{}; const hosts=[context,context.formContext,context.pageContext,context.variableContext,context.variables,context.tempVariables,context.runtimeContext,context.dataContext].filter(Boolean); const methods=['setFieldValue','setFormFieldValue','setFieldData','setValue','setVariableValue','setVariable','setTempVariableValue','setTempVariable','updateFieldValue','updateVariableValue']; for(let h=0;h<hosts.length;h+=1){for(let m=0;m<methods.length;m+=1){const fn=hosts[h][methods[m]]; if(typeof fn==='function'){try{const result=fn.call(hosts[h],name,value); if(result!==false){return true;}}catch(error){}}}} const objectMethods=['setFieldsValue','setFieldValues','setValues','setVariables','setTempVariables']; for(let h=0;h<hosts.length;h+=1){for(let m=0;m<objectMethods.length;m+=1){const fn=hosts[h][objectMethods[m]]; if(typeof fn==='function'){try{const payload:any={};payload[name]=value;const result=fn.call(hosts[h],payload); if(result!==false){return true;}}catch(error){}}}} fieldsValues[name]=value; return true;}
  readTarget(target:any){const candidates=target&&target.candidates?target.candidates:[]; const context=this.props.context||{}; const fieldsValues=this.props.fieldsValues||{}; for(let i=0;i<candidates.length;i+=1){const k=candidates[i]; if(fieldsValues&&fieldsValues[k]!==undefined){return fieldsValues[k];} if(context&&typeof context.getFieldValue==='function'){try{const v=context.getFieldValue(k); if(v!==undefined&&v!==null&&this.toString(v)!==''){return v;}}catch(error){}}} return '';}

  render() { const config=this.getConfig(); return <div className="yft-panel"><style>{this.getStyles()}</style><div className="yft-header"><div><div className="yft-title">{config.titleText}</div><div className="yft-subtitle">{config.subtitleText}</div></div>{this.renderBadge(config)}</div>{this.renderBody(config)}</div>; }
  renderBadge(config:any) { if(TEMPLATE_KIND==='multi-entry-tag-input'){return <span className="yft-badge">{this.state.tags.length} / {config.maxTags}</span>;} if(TEMPLATE_KIND==='checklist-compliance-block'){const total=config.items.length; const done=config.items.filter((x:any)=>this.state.checklist[x.id]===true).length; return <span className="yft-badge">{done} / {total}</span>;} if(this.state.rows&&this.state.rows.length){return <span className="yft-badge">{this.state.rows.length} records</span>;} return null; }
  renderBody(config:any) { if(this.state.loading){return <div className="yft-loading">Loading...</div>;} if(this.state.errorText){return <div className="yft-empty yft-error">{this.state.errorText}</div>;} if(TEMPLATE_KIND==='multi-entry-tag-input'){return this.renderTagInput(config);} if(TEMPLATE_KIND==='checklist-compliance-block'){return this.renderChecklist(config);} if(TEMPLATE_KIND==='hierarchical-selector'){return this.renderTree(config);} if(TEMPLATE_KIND==='dependent-selector'){return this.renderDependent(config);} if(TEMPLATE_KIND==='exception-alert-panel'){return this.renderAlerts(config);} if(TEMPLATE_KIND==='related-record-summary-grid'){return this.renderRelated(config);} return this.renderTimeline(config); }
  renderTagInput(config:any) { const readonly=this.props.readonly; return <div><div className="yft-chiprow">{this.state.tags.map((tag:string,index:number)=><span className="yft-chip" key={tag+'-'+index}>{tag}{!readonly?<button type="button" onClick={()=>{const next=this.state.tags.slice();next.splice(index,1);this.persistTags(next,config);}}>x</button>:null}</span>)}</div>{!readonly?<input className="yft-input" value={this.state.textValue} placeholder={config.placeholderText} onChange={(e)=>this.setState({textValue:e.target.value})} onKeyDown={(e)=>{if(e.key==='Enter'){e.preventDefault();const value=this.toString(this.state.textValue);if(value&&this.state.tags.length<config.maxTags){this.persistTags(this.state.tags.concat([value]),config);}}}} />:null}</div>; }
  renderChecklist(config:any) { if(!config.items.length){return <div className="yft-empty">Configure checklist items JSON.</div>;} const total=config.items.length; const done=config.items.filter((x:any)=>this.state.checklist[x.id]===true).length; return <div>{config.items.map((item:any)=><label className="yft-row" key={item.id}><input type="checkbox" checked={this.state.checklist[item.id]===true} disabled={this.props.readonly} onChange={(e)=>{const map:any={};Object.keys(this.state.checklist).forEach((k)=>map[k]=this.state.checklist[k]);map[item.id]=e.target.checked;this.persistChecklist(map,config);}} /> <span className="yft-row-title">{item.label}{item.required!==false?' *':''}</span>{item.helper?<div className="yft-row-meta">{item.helper}</div>:null}</label>)}{config.showProgress?<div className="yft-progress"><span style={{width:(total?Math.round(done/total*100):0)+'%'}} /></div>:null}{this.state.validationMessage?<div className="yft-validation">{this.state.validationMessage}</div>:null}</div>; }
  renderTimeline(config:any) { const rows=this.state.rows||[]; if(!rows.length){return <div className="yft-empty">{config.emptyText}</div>;} return <div className="yft-timeline">{rows.map((row:any,index:number)=><div className="yft-event" key={index}><div className="yft-row-title">{this.toString(this.readField(row,config.titleField))||this.toString(this.readField(row,config.statusField))||'Event'}</div><div className="yft-row-meta">{this.formatDate(this.readField(row,config.eventDateField))} {this.toString(this.readField(row,config.actorField))}</div>{this.toString(this.readField(row,config.commentFieldName))?<div className="yft-row-meta">{this.toString(this.readField(row,config.commentFieldName))}</div>:null}</div>)}</div>; }
  renderRelated(config:any) { const rows=this.state.rows||[]; if(!rows.length){return <div className="yft-empty">{config.emptyText}</div>;} const fields=config.displayedFields.length?config.displayedFields:[config.titleField,config.statusField,config.commentFieldName].filter(Boolean); if(config.displayType==='table'){return <table className="yft-table"><thead><tr>{fields.map((f:string)=><th key={f}>{f}</th>)}</tr></thead><tbody>{rows.map((row:any,i:number)=><tr key={i}>{fields.map((f:string)=><td key={f}>{this.toString(this.readField(row,f))}</td>)}</tr>)}</tbody></table>;} return <div className="yft-grid">{rows.map((row:any,i:number)=><div className="yft-card" key={i}>{fields.map((f:string,index:number)=><div key={f} className={index===0?'yft-row-title':'yft-row-meta'}>{index>0?f+': ':''}{this.toString(this.readField(row,f))}</div>)}</div>)}</div>; }
  renderTree(config:any) { const rows=this.state.rows||[]; if(!rows.length){return <div className="yft-empty">{config.emptyText}</div>;} const byParent:any={}; rows.forEach((r:any)=>{const p=this.toString(this.readField(r,config.parentField)); if(!byParent[p]){byParent[p]=[];} byParent[p].push(r);}); const renderNodes=(parent:string):any=> (byParent[parent]||[]).map((row:any,index:number)=>{const value=this.toString(this.readField(row,config.valueField)||this.readField(row,config.idField)); const label=this.toString(this.readField(row,config.labelField))||value; const selected=this.state.selected.indexOf(value)!==-1; return <div className="yft-tree-node" key={value||index}><button type="button" className={selected?'yft-btn yft-btn-active':'yft-btn'} onClick={()=>{let next=config.multiSelect?this.state.selected.slice():[]; const pos=next.indexOf(value); if(pos===-1){next.push(value);}else{next.splice(pos,1);} this.writeTarget(config.saveTarget,JSON.stringify(next)); this.setState({selected:next});}}>{label}</button><div className="yft-tree-children">{renderNodes(value)}</div></div>;}); return <div>{renderNodes('')}</div>; }
  renderDependent(config:any) { const rows=this.state.rows||[]; const parents=this.unique(rows.map((r:any)=>this.toString(this.readField(r,config.parentField))).filter(Boolean)); const children=this.unique(rows.filter((r:any)=>this.toString(this.readField(r,config.parentField))===this.state.parentValue).map((r:any)=>this.toString(this.readField(r,config.childField))).filter(Boolean)); return <div className="yft-grid"><div><div className="yft-muted">Parent</div><select className="yft-select" value={this.state.parentValue} disabled={this.props.readonly} onChange={(e)=>{this.setState({parentValue:e.target.value,childValue:''});this.writeTarget(config.parentTarget,e.target.value);this.writeTarget(config.childTarget,'');}}><option value="">Select</option>{parents.map((p)=><option key={p} value={p}>{p}</option>)}</select></div><div><div className="yft-muted">Child</div><select className="yft-select" value={this.state.childValue} disabled={this.props.readonly||!this.state.parentValue} onChange={(e)=>{this.setState({childValue:e.target.value});this.writeTarget(config.childTarget,e.target.value);}}><option value="">Select</option>{children.map((c)=><option key={c} value={c}>{c}</option>)}</select></div></div>; }
  renderAlerts(config:any) { const rows=this.state.rows||[]; const rules=config.rules; let alerts:any[]=[]; if(rules.length){ rows.forEach((row:any)=>{ rules.forEach((rule:any)=>{const field=this.toString(rule.field); const op=this.toString(rule.operator||'equals'); const expected=this.toString(rule.value); const actual=this.toString(this.readField(row,field)); const hit=op==='notEmpty'?actual!=='':op==='empty'?actual==='':op==='contains'?actual.indexOf(expected)!==-1:actual===expected; if(hit){alerts.push({title:this.toString(rule.title)||this.toString(this.readField(row,config.titleField))||'Alert',message:this.toString(rule.message)||field+' '+op+' '+expected,severity:this.toString(rule.severity)||'warning'});} }); });} else { alerts=rows.map((row:any)=>({title:this.toString(this.readField(row,config.titleField))||'Alert',message:this.toString(this.readField(row,config.commentFieldName)),severity:this.toString(this.readField(row,config.statusField))||'warning'})); } if(!alerts.length){return <div className="yft-empty">{config.emptyText}</div>;} return <div>{alerts.slice(0,config.maxItems).map((a:any,i:number)=><div className={'yft-alert yft-alert-'+(a.severity||'warning')} key={i}><div className="yft-row-title">{a.title}</div>{a.message?<div className="yft-row-meta">{a.message}</div>:null}</div>)}</div>; }
  getStyles() { return [".yft-panel{box-sizing:border-box;width:100%;min-width:0;background:#fff;border:1px solid #e5eaf3;border-radius:8px;padding:16px;color:#111827;font-family:Inter,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;box-shadow:0 1px 2px rgba(16,24,40,.04);}", ".yft-panel *{box-sizing:border-box;}", ".yft-header{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:14px;}", ".yft-title{font-size:16px;line-height:22px;font-weight:700;color:#101828;}", ".yft-subtitle{font-size:12px;line-height:18px;color:#667085;margin-top:3px;}", ".yft-badge{border:1px solid #e5eaf3;background:#f8fafc;border-radius:999px;padding:3px 9px;font-size:12px;color:#475467;}", ".yft-empty,.yft-loading{min-height:120px;display:flex;align-items:center;justify-content:center;text-align:center;border:1px dashed #d8deea;border-radius:8px;background:#f8fafc;color:#667085;font-size:13px;line-height:20px;padding:20px;}", ".yft-error{color:#b42318;background:#fff7f6;border-color:#fecdca;}", ".yft-row{border:1px solid #e5eaf3;border-radius:8px;padding:10px 12px;background:#fff;margin-bottom:8px;}", ".yft-row-title{font-size:13px;font-weight:700;line-height:18px;color:#101828;}", ".yft-row-meta{font-size:12px;line-height:18px;color:#667085;margin-top:3px;}", ".yft-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;}", ".yft-card{border:1px solid #e5eaf3;border-radius:8px;background:#fff;padding:12px;}", ".yft-table{width:100%;border-collapse:separate;border-spacing:0;border:1px solid #e5eaf3;border-radius:8px;overflow:hidden;}", ".yft-table th{background:#f8fafc;color:#475467;font-size:12px;text-align:left;padding:9px;border-bottom:1px solid #e5eaf3;}", ".yft-table td{font-size:13px;color:#344054;padding:9px;border-bottom:1px solid #eef2f7;}", ".yft-chiprow{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;}", ".yft-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid #c8dafc;background:#edf4ff;color:#0d4fb3;border-radius:999px;padding:5px 9px;font-size:13px;line-height:18px;}", ".yft-chip button{appearance:none;border:0;background:transparent;color:#0d4fb3;cursor:pointer;font-size:15px;line-height:15px;padding:0;}", ".yft-input,.yft-textarea,.yft-select{width:100%;border:1px solid #d8deea;border-radius:8px;padding:9px 10px;font-size:13px;line-height:20px;color:#101828;background:#fff;outline:none;font-family:inherit;}", ".yft-textarea{min-height:86px;resize:vertical;}", ".yft-input:focus,.yft-textarea:focus,.yft-select:focus{border-color:#146ff6;box-shadow:0 0 0 3px rgba(20,111,246,.12);}", ".yft-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;}", ".yft-btn{appearance:none;border:1px solid #d8deea;background:#fff;color:#344054;border-radius:8px;padding:8px 11px;font-size:13px;font-weight:650;cursor:pointer;}", ".yft-btn-active{border-color:#146ff6;background:#edf4ff;color:#0d4fb3;}", ".yft-btn-primary{border-color:#146ff6;background:#146ff6;color:#fff;}", ".yft-validation{margin-top:10px;border:1px solid #fecdca;background:#fff7f6;color:#b42318;border-radius:8px;padding:9px 10px;font-size:12px;line-height:18px;}", ".yft-progress{height:8px;background:#eef2f7;border-radius:999px;overflow:hidden;margin-top:10px;}", ".yft-progress span{display:block;height:100%;background:#146ff6;border-radius:999px;}", ".yft-timeline{position:relative;padding-left:18px;}", ".yft-timeline:before{content:\"\";position:absolute;left:5px;top:4px;bottom:4px;width:2px;background:#e5eaf3;}", ".yft-event{position:relative;margin-bottom:12px;border:1px solid #e5eaf3;border-radius:8px;padding:10px 12px;background:#fff;}", ".yft-event:before{content:\"\";position:absolute;left:-17px;top:14px;width:10px;height:10px;border-radius:999px;background:#146ff6;box-shadow:0 0 0 4px #edf4ff;}", ".yft-alert{border:1px solid #fecdca;background:#fff7f6;border-radius:8px;padding:10px 12px;margin-bottom:8px;color:#7a271a;}", ".yft-alert-warning{border-color:#fedf89;background:#fffbeb;color:#7a4b00;}", ".yft-alert-info{border-color:#b2ddff;background:#eff8ff;color:#1849a9;}", ".yft-alert-success{border-color:#abefc6;background:#f0fdf4;color:#067647;}", ".yft-tree-node{border:1px solid #e5eaf3;border-radius:8px;padding:9px 10px;margin-bottom:8px;background:#fff;}", ".yft-tree-children{margin-left:18px;margin-top:8px;}", ".yft-muted{color:#667085;font-size:12px;line-height:18px;}", "@media(max-width:640px){.yft-panel{padding:14px}.yft-header{flex-direction:column}.yft-table{display:block;overflow-x:auto}.yft-actions{flex-direction:column}.yft-btn{width:100%;text-align:center}}"].join(''); }
}

User guide

Download guide
# Related Record Summary Grid

## Short Description

Related Record Summary Grid - reusable Yeeflow component for showing related records inline as cards or table rows.

## Purpose / What This Is For

Shows key related records without leaving the current form or dashboard.

## Supported Placement

Approval form; Dashboard

## When To Use

Use this control when teams need a reusable Yeeflow control for case context, order context, asset context, and related list summaries.

## Recommended Defaults

Start with the required source/save parameters, keep labels concise, and test with a small data set before publishing.

## Input Parameters Overview

| Parameter | Type | Purpose |
| --- | --- | --- |
| `targetListId` | `variable` | Related data list ID. |
| `relationField` | `variable` | Field used to match relation. |
| `relationValue` | `variable` | Current relation value. |
| `displayedFieldsJson` | `variable` | JSON/comma list of fields to display. |
| `displayType` | `string` | card or table. |
| `titleField` | `variable` | Primary field. |
| `statusField` | `variable` | Status field. |
| `titleText` | `string` | Panel title. |
| `subtitleText` | `string` | Panel subtitle. |
| `maxItems` | `string` | Max records. |
| `emptyText` | `string` | Empty text. |
| `filterExpression` | `variable` | Optional filter. |

## Detailed Parameter Explanation

Configure expression-editor parameters with Yeeflow fields, variables, temp variables, or static values as appropriate. Writable target parameters save output values and are resolved defensively at runtime. Plain text parameters control display labels and helper text.

## Step-By-Step Setup

1. Add a Custom Code control to the supported Yeeflow placement.
2. Paste the `related-record-summary-grid.tsx` code file.
3. Configure required data source, field, and save-target parameters.
4. Configure labels, limits, and display options.
5. Preview with realistic records.
6. Confirm readonly, empty, and validation states.
7. Publish when behavior is correct.

## Result / Expected Output

A compact card grid or table of related records filtered by relation field/value.

## Real Business Examples

- Procurement approval and vendor review.
- Case, request, or task management.
- Policy, compliance, and operational control checks.
- Department, location, product, or category-driven workflows.

## Notes / Assumptions / Limitations

- Runtime query behavior uses `yeeSDKClient.lists.queryItems(...)` where list data is needed.
- Expression-editor values are normalized from strings, booleans, objects, arrays, and variable selector values.
- Writable targets use defensive setter paths and a local `fieldsValues` fallback.
- Tenant-specific query filter shapes may require adjustment in `filterExpression`.

## Testing Checklist

- Required parameters configured.
- Empty state displays safely.
- Existing saved values initialize correctly when applicable.
- Save targets update as expected when applicable.
- Readonly mode prevents editing.
- Narrow layout remains readable.

## Troubleshooting

If data does not appear, verify list ID, field IDs, filter expression, and permissions. If saving does not work, confirm the target parameter was selected as a writable form field or variable and that the field exists on the form.

Example configuration

Download config
# Related Record Summary Grid Example Config

| Parameter | Example Value |
| --- | --- |
| `targetListId` | `203...` |
| `relationField` | `RequestId` |
| `relationValue` | `{{ListDataID}}` |
| `displayedFieldsJson` | `["Title","Status"]` |
| `displayType` | `card` |
| `titleField` | `Title` |
| `statusField` | `Status` |
| `titleText` | `Related Records` |
| `subtitleText` | `Linked records` |
| `maxItems` | `6` |
| `emptyText` | `No related records` |
| `filterExpression` | `` |

Adjust field IDs and list IDs to match the target Yeeflow application.