Target type DashboardLayout
The custom widget can be rendered in Dashboard layout. The custom widget with target type 'DashboardLayout' is visible in mobile app.
Coding guidelines
pageContextThe page context provided has following content
{
instanceType: "Instance type name for which the layout is configured",
typePluralName: "Instance type plural name",
typeSingularName: "Instance type singular name",
}
- Along with this,
pageContextcontains few more properties that are common to all widget types. For details check: common pageContext - Any API calls made from the widget will use the logged-in users' context.
Typical use cases
- Used in displaying custom widgets for the Dashboard layout. Role based visibility can be controlled by Dashboard layout configurations.
- Typically used to show non-standard data visualization or user interactions that require data fetching or update requirements with additional business logic or validations.
Sample Code
- Here's an example of a Dashboard layout widget that:
- fetches instances of a particular type using in-built
useProtrakApihook and a custom promise function - displays data in a tabular form
- fetches instances of a particular type using in-built
const lifecycleUrl = '/lifecycles/Patent Application';
const docketId = 'DocketId';
const patentInstanceName = 'patent';
const invTOPatent = 'InvTOPatent';
const lastStateName = 'Granted';
const docketIdColumn = { name: 'Docket Id', displayName: ' Docket Id ' };
function PatentAlertDashboard(pageContext) {
const { protrakUtils, protrakComponents } = React.useContext(customWidgetContext);
const { useProtrakApi } = protrakUtils;
const { Container, Spinner, ErrorModal } = protrakComponents;
const { state: responsePatents, run } = useProtrakApi({
requestConfig: getStatesPromise
});
if (responsePatents.isLoading || !responsePatents.data) {
return (
<Container>
<Spinner />
</Container>
);
}
if (!responsePatents.isLoading) {
if (responsePatents.isError) {
return (
<ErrorModal message="Error while fetching data..." />
);
}
if (
responsePatents.data &&
responsePatents.data.states &&
responsePatents.data.states.length > 0
) {
const states = responsePatents.data.states.filter(state => state.name !== lastStateName);
return (
<TabularData
columns={[docketIdColumn, ...states]}
/>
);
} else {
return <RenderTable />;
}
}
}
function TabularData({ columns }) {
const { protrakUtils, protrakComponents } = React.useContext(customWidgetContext);
const {
useProtrakApi,
differenceInDaysFromToday
} = protrakUtils;
const {
Container,
Spinner,
ErrorModal
} = protrakComponents;
const { state: patents, run } = useProtrakApi({
requestConfig: getPatentsPromise
});
if (patents.isLoading || !patents.data) {
return (
<Container>
<Spinner />
</Container>
);
}
if (!patents.isLoading) {
if (patents.isError) {
return (
<ErrorModal message="Error while fetching data..." />
);
}
if (patents.data && patents.data.items && patents.data.items.length > 0) {
let groupedData = [];
const alertPatents = patents.data.items.filter(patent => {
if (patent && patent.state && patent.state.name === 'Granted') {
return false;
}
if (patent && patent.activities && patent.activities.length > 0) {
const activitiesCount = patent.activities.length;
patent.activities.sort((a, b) => new Date(a.endTime) - new Date(b.endTime));
const activity = patent.activities[activitiesCount - 1];
if (differenceInDaysFromToday(activity.startTime) >= 30) {
return true;
}
}
return false;
});
alertPatents.forEach(patent => {
const docketIdAttr =
patent &&
patent.attributes.find(attribute => {
return attribute.name === docketId;
});
const inventionRefAttr =
patent &&
patent.attributes.find(attribute => {
return attribute.name === invTOPatent;
});
const activitiesCount = patent.activities.length;
const state = patent.activities[activitiesCount - 1].toState;
groupedData.push({
patentId: patent.id,
docketId: docketIdAttr,
invention: inventionRefAttr,
state: state
});
});
if (groupedData.length > 0) {
return <Data columns={columns} patents={groupedData} />;
} else {
return <RenderTable columns={columns} />;
}
} else {
return <RenderTable columns={columns} />;
}
}
}
const Data = ({ columns, patents }) => {
const pageSize = 10;
const cellStyle = {
border: '1px solid #dddddd',
textAlign: 'center',
padding: '8px',
minWidth: '5rem'
};
const [take, setTake] = React.useState(1);
const [isError, setIsError] = React.useState(false);
//check if next page is available and also if index out of bound
const trimmedPatents = patents.slice(
take * pageSize - pageSize,
take * pageSize
);
const totalNumberOfPages = Math.ceil(patents.length / pageSize);
const onChange = pageNumber => {
if (totalNumberOfPages >= pageNumber && pageNumber > 0) {
setIsError(false);
setTake(pageNumber);
} else {
setIsError(true);
}
};
return (
<RenderTable
columns={columns}
body={
<tbody>
{trimmedPatents.map(patent => {
return (
<tr key={patent.patentId}>
{columns.map(column => {
if (column.name === 'Docket Id') {
const appUrl = document.location.origin.includes(
'localhost'
)
? '/Protrak'
: '';
return (
<td key={patent.patentId + column} style={cellStyle}>
{patent.docketId && patent.invention ? (
<a
href={`${appUrl}/#view_${patent.patentId
}`}
target="_blank"
>
{patent.docketId.textValue}
</a>
) : (
'-'
)}
</td>
);
}
if (column.name === patent.state.name) {
return (
<td
key={patent.patentId + column}
style={{ ...cellStyle, backgroundColor: 'red' }}
/>
);
}
return (
<td
key={patent.patentId + column}
style={{ ...cellStyle }}
/>
);
})}
</tr>
);
})}
</tbody>
}
statusBar={
<div style={{ paddingTop: '1rem' }}>
{' '}
<div style={{ display: 'flex', justifyContent: 'center' }}>
<button disabled={!(take > 1)} onClick={() => setTake(take - 1)}>
{' '}
‹
</button>
<span style={{ padding: '0.5rem' }}>Page</span>{' '}
<input
type="number"
onChange={e => onChange(e.target.value)}
onBlur={e => {
const page = e.target.value ? Number(e.target.value) : 0;
if (page !== take) {
onChange(page);
}
}}
value={take}
style={{
border: isError ? '1px solid red' : '1px solid grey',
padding: '0.35rem 0.9rem',
width: '3rem'
}}
/>{' '}
<span style={{ padding: '0.5rem' }}>of {totalNumberOfPages}</span>
<button
disabled={!(take * pageSize < patents.length)}
onClick={() => setTake(take + 1)}
>
{' '}
›
</button>
</div>
</div>
}
/>
);
};
const RenderTable = ({ columns = [], body, statusBar }) => {
const { protrakUtils } = React.useContext(customWidgetContext);
const cellStyle = {
border: '1px solid #dddddd',
textAlign: 'left',
padding: '8px',
minWidth: '5rem'
};
const { getDate } = protrakUtils;
const date = new Date(getDate().toString());
const formattedDate =
date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear();
const message = `Today's Date ${formattedDate}`;
return (
<div style={{ overflow: 'auto' }}>
<div
style={{ display: 'flex', marginTop: '1rem', marginBottom: '0.5rem' }}
>
<div style={{ width: '100%' }}>
<i className="fa fa-info-circle" style={{ color: '#2383dc' }} />
{message}
</div>{' '}
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div
style={{
backgroundColor: 'red',
width: '1.5rem',
height: '1.5rem',
marginLeft: '1rem',
marginRight: '0.4rem'
}}
/>
<span style={{ marginTop: '0.2rem' }}>
Patent is in current state from the last 30 days
</span>
</div>
</div>
</div>
<table
style={{
border: '1px solid grey',
borderCollapse: 'collapse',
width: '100%'
}}
>
<thead>
<tr>
{columns.map(column => {
return (
<th key={column.name} style={cellStyle}>
{' '}
{column.displayName}
</th>
);
})}
</tr>
</thead>
{body}
</table>
{statusBar}
</div>
);
};
const getPatentsPromise = () => {
return {
endpoint: 'instances', config: {
method: 'GET',
params: {
instanceTypeName: patentInstanceName,
'attributes[0]': docketId,
'attributes[1]': invTOPatent,
Skip: 0,
Take: 999,
'ActivityQuery.Type': 1
}
}
};
};
const getStatesPromise = () => {
return {
endpoint: lifecycleUrl, config: {
method: 'GET'
}
};
};
Here's a preview of how this custom widget looks like.
