Overview
What this template helps teams build
Overview
The Activity Timeline template is a reusable custom code control concept for Yeeflow builders. It helps teams add a focused interface pattern for record history and operational activity tracking while keeping the asset in review before public launch.
Key capabilities
- Supports operational work scenarios
- Designed for use in Application page, Record detail, Dashboard
- Includes TSX source, user guidance, and example configuration
- Prepared as a preview asset for technical review and future public documentation
Recommended use cases
- record history and operational activity tracking
- 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.
Developer reference
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.
Source preview
Code preview
import React from 'react';
const TEMPLATE_KIND = 'activity-timeline';
const TEMPLATE_TITLE = 'Activity Timeline';
const TEMPLATE_DESC = 'Activity Timeline - reusable Yeeflow timeline for record events, changes, comments, and operational actions.';
const WRITABLE_TARGETS = [];
export class CodeInApplication implements CodeInComp {
description() { return TEMPLATE_DESC; }
requiredFields(params?: CodeInParams) { return []; }
inputParameters(): InputParameter[] { return [
{"id": "sourceListId", "name": "Source List ID", "type": "variable", "desc": "Activity list ID.", "example": "203..."},
{"id": "recordIdField", "name": "Record ID Field", "type": "variable", "desc": "Relation field.", "example": "CaseId"},
{"id": "recordIdValue", "name": "Record ID Value", "type": "variable", "desc": "Current record id.", "example": "{{ListDataID}}"},
{"id": "eventDateField", "name": "Event Date Field", "type": "variable", "desc": "Event timestamp.", "example": "CreatedTime"},
{"id": "actorField", "name": "Actor Field", "type": "variable", "desc": "Actor field.", "example": "Actor"},
{"id": "titleField", "name": "Title Field", "type": "variable", "desc": "Event title.", "example": "ActivityTitle"},
{"id": "descriptionField", "name": "Description Field", "type": "variable", "desc": "Event detail.", "example": "Description"},
{"id": "statusField", "name": "Status Field", "type": "variable", "desc": "Status/type field.", "example": "ActivityType"},
{"id": "titleText", "name": "Title Text", "type": "string", "desc": "Panel title.", "example": "Activity Timeline"},
{"id": "maxItems", "name": "Max Items", "type": "string", "desc": "Max events.", "example": "10"},
{"id": "emptyText", "name": "Empty Text", "type": "string", "desc": "Empty text.", "example": "No activity yet"},
{"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)||'Recent activity and record events.',
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(''); }
}Implementation notes
User guide
# Activity Timeline ## Short Description Activity Timeline - reusable Yeeflow timeline for record events, changes, comments, and operational actions. ## Purpose / What This Is For Displays a unified timeline of events around a case, request, asset, or operational record. ## Supported Placement Approval form; Dashboard ## When To Use Use this control when teams need a reusable Yeeflow control for case activity, record history, comments, status changes, and action logs. ## 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 | | --- | --- | --- | | `sourceListId` | `variable` | Activity list ID. | | `recordIdField` | `variable` | Relation field. | | `recordIdValue` | `variable` | Current record id. | | `eventDateField` | `variable` | Event timestamp. | | `actorField` | `variable` | Actor field. | | `titleField` | `variable` | Event title. | | `descriptionField` | `variable` | Event detail. | | `statusField` | `variable` | Status/type field. | | `titleText` | `string` | Panel title. | | `maxItems` | `string` | Max events. | | `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 `activity-timeline.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 vertical event timeline sorted by event date. ## 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.
Configuration
Example configuration
# Activity Timeline Example Config
| Parameter | Example Value |
| --- | --- |
| `sourceListId` | `203...` |
| `recordIdField` | `CaseId` |
| `recordIdValue` | `{{ListDataID}}` |
| `eventDateField` | `CreatedTime` |
| `actorField` | `Actor` |
| `titleField` | `ActivityTitle` |
| `descriptionField` | `Description` |
| `statusField` | `ActivityType` |
| `titleText` | `Activity Timeline` |
| `maxItems` | `10` |
| `emptyText` | `No activity yet` |
| `filterExpression` | `` |
Adjust field IDs and list IDs to match the target Yeeflow application.



