Summary
- Team Sharing can be used to share any kind of data including custom objects
The primary use case for TeamSharing is to allow different AdapTable instances to share AdapTable Objects, e.g. Layouts, Format Columns, etc.
However, it is also possible to share bespoke object structures and values.
Caution
- AdapTable does not provide dedicated UI components for sharing Custom Objects
- Each user should create their own UI components optimised for their specific use case
AdapTable is very flexible and unopinionated regarding the structure of the shared data, so anything can be Team Shared if required.
Deep Dive
Understanding Custom Team Sharing
Shared objects in AdapTable are of type SharedEntity defined as follows:
| Property | Description |
|---|---|
| ChangedAt | Last time when the object was changed |
| ChangedBy | Last User who changed the object |
| Description | Description of object being shared |
| Entity | Actual Adaptable Object being shared |
| EntityDependencyIds | Ids of direct entity dependencies |
| EntityType | Type of shared entity (either Adaptable specific or custom object) |
| Module | Adaptable Module to which object belongs |
| Revision | Revision - incremental for 'Active', always 1 for 'Snapshot' |
| Timestamp | When the object was shared |
| Type | 'Snapshot' (for 1-time sharing) or 'Active' (for continuous sharing) |
| UserName | User who shared the object |
| IsReadOnly | Sets Entity to ReadOnly (overwriting a Strategy Entitlement of 'Full') |
The Shared Entity can be either:
- An
AdapTable SharedEntity(which extends an Adaptable Object) - A
Custom Shared Entity- essentially of any Type
In addition, AdapTable provides several API methods that can be used when sharing custom objects:
getLoadedCustomSharedEntities(): returns all the locally loadedCustomSharedEntitiesshareCustomEntity(): shared the givenCustomSharedEntitiesimportSharedEntry(): imports the given SharedEntry. In case ofCustomSharedEntities, the import logic will be delegated to TeamSharingOptions.handleCustomSharedEntityImport()
While import of shared AdaptableObjects (AdaptableSharedEntity flavour of the SharedEntity) is automatically handled by Adaptable, the import of custom shared objects (CustomSharedEntity flavour of the SharedEntity) needs to be handled on a case-by-case basis.
Invoking TeamSharingApi.importSharedEntry() with a CustomSharedEntry will call this function.
teamSharingOptions: {
enableTeamSharing: true,
loadSharedEntities: async () => {
// ...
},
persistSharedEntities: async (entries) => {
// ...
},
handleCustomSharedEntityImport: (sharedEntity: CustomSharedEntity) => {
if (sharedEntity.Uuid === CUSTOM_SHARE_KEY || sharedEntity.Name === 'myCustomObject') {
// implement object import
}
}
}Hint
See the provided demo showcasing the import & export of custom objects.
- This example illustrates how Team Sharing can be used to share custom objects
- Each user may increment/decrement their local value and choose to either:
- push their local state value to the remote common state
- import the remote state value and overwrite their local value
Expand to see how custom Team Sharing is handled
- Every second we poll the remote TeamSharing state, to ensure the
TeamSharingApi.getCustomSharedEntities()returns up-to-date values.
export const onAdaptableReady = ({adaptableApi}: AdaptableReadyInfo) => {
const timerId = setInterval(() => {
// reload TeamSharing state
adaptableApi.teamSharingApi.refreshTeamSharing();
// re-render toolbar buttons to force the update of the remote state elements
adaptableApi.dashboardApi.refreshDashboard();
}, 1000);
adaptableApi.eventApi.on('AdaptableDestroy', () => {
clearInterval(timerId);
});
};- Updating the remote state is done by invoking the
shareCustomEntity()method. Notice that we have a fixed technical ID (Uuid) in order to be able to reference it later when re-importing it.
onClick: (button, context) => {
const currentLocalValue = getLocalMutableShare(userName);
context.adaptableApi.teamSharingApi.shareCustomEntity(
currentLocalValue,
{
Uuid: CUSTOM_SHARE_KEY,
Name: 'Counter',
Description: 'Just a simple example of a shared custom state',
}
);
},- Querying the remote state is done by invoking the
getLoadedCustomSharedEntities()method (the local state is kept up-to-date, see 1. above)
const getRemoteCustomSharedEntity = (
adaptableApi: AdaptableApi
): CustomSharedEntity | undefined => {
// TeamSharing state is refreshed every 2 seconds, see `onAdaptableReady()` handler
return adaptableApi.teamSharingApi
.getLoadedCustomSharedEntities()
.find(sharedEntity => sharedEntity.Uuid === CUSTOM_SHARE_KEY);
};- Importing a remote state value is done by invoking
importSharedEntry(). The actual importing logic is provided via the TeamSharingOptions.handleCustomSharedEntityImport() callback.
const remoteCustomSharedEntity = getRemoteCustomSharedEntity(
context.adaptableApi
);
if (remoteCustomSharedEntity) {
context.adaptableApi.teamSharingApi.importSharedEntry(
remoteCustomSharedEntity
);
}
teamSharingOptions: {
enableTeamSharing: true,
handleCustomSharedEntityImport: (sharedEntity: CustomSharedEntity) => {
if (sharedEntity.Uuid === CUSTOM_SHARE_KEY) {
setLocalState(userName, sharedEntity.Entity);
}
},
},- As Alice increment the local value from 0 to 1
- Switch to Bob and note that his local value is still 0
- Switch back to Alice and now click "Push to Team Sharing"
- Switch back to Bob and note that his local value is still 0 but the next tab shows that Alice updated the value to 1
- Click 'Import Remote Value' and note that Bob now has a local value of 1 (same as Alice)
- Update the Local Value from 1 to 2 and then click "Push to Team Sharing"
- Switch back to Alice and note that while the local value 1, it says 2 is available which can be fetched by clicking "Import Remote Value"
import { InitialState, AdaptableOptions, AdaptableApi, CustomSharedEntity, } from '@adaptabletools/adaptable'; import {WebFramework} from './rowData'; // use a common Team Sharing state key for both users const TEAM_SHARING_STATE_KEY = 'CustomTeamSharingDemo_TeamSharingState'; const CUSTOM_SHARE_KEY = 'demoCustomKey'; const getLocalMutableShare = ( userName: 'Alice' | 'Bob' ): { value: number; lastUpdatedBy: string; } => { const localState = localStorage.getItem( `CustomTeamSharingDemo_${userName}_localState` ); if (!localState) { return { value: 0, lastUpdatedBy: 'none', }; } return JSON.parse(localState); }; const mutateLocalState = (userName: 'Alice' | 'Bob', step: 1 | -1) => { let localState = getLocalMutableShare(userName); localState = { value: localState.value + step, lastUpdatedBy: userName, }; localStorage.setItem( `CustomTeamSharingDemo_${userName}_localState`, JSON.stringify(localState) ); }; const setLocalState = ( userName: 'Alice' | 'Bob', newValue: { value: number; lastUpdatedBy: string; } ) => { localStorage.setItem( `CustomTeamSharingDemo_${userName}_localState`, JSON.stringify(newValue) ); }; const getRemoteCustomSharedEntity = ( adaptableApi: AdaptableApi ): CustomSharedEntity | undefined => { // TeamSharing state is refreshed every 2 seconds, see `onAdaptableReady()` handler return adaptableApi.teamSharingApi .getLoadedCustomSharedEntities() .find(sharedEntity => sharedEntity.Uuid === CUSTOM_SHARE_KEY); }; export const getAdaptableOptionsForUser = ( userName: 'Alice' | 'Bob', updateCurrentUser: (userName: 'Alice' | 'Bob') => void ): AdaptableOptions<WebFramework> => { const commonInitialState: InitialState = { Dashboard: { Revision: Date.now(), Tabs: [ { Name: 'Configuration', Toolbars: ['CurrentUser', 'LocalCustomState', 'RemoteCustomState'], }, ], ModuleButtons: ['FormatColumn', 'TeamSharing', 'SettingsPanel'], }, Layout: { CurrentLayout: 'Standard Layout', Layouts: [ { TableColumns: [ 'name', 'language', 'github_stars', 'license', 'has_projects', 'has_pages', 'created_at', 'has_wiki', 'updated_at', 'pushed_at', 'github_watchers', 'week_issue_change', ], Name: 'Standard Layout', AutoSizeColumns: true, }, ], }, }; const userSpecificInitialState = userName === 'Alice' ? { FormatColumn: { FormatColumns: [ { Name: 'formatColumn-name', Scope: { ColumnIds: ['name'], }, Style: { BackColor: 'lightblue', ForeColor: 'brown', }, }, ], }, Theme: { CurrentTheme: 'light', }, } : // Bob { FormatColumn: { FormatColumns: [ { Name: 'formatColumn-language', Scope: { ColumnIds: ['language'], }, Style: { BackColor: 'yellow', ForeColor: 'black', }, }, ], }, Theme: { CurrentTheme: 'dark', }, }; return { primaryKey: 'id', adaptableId: 'Custom Objects Team Sharing Demo', adaptableStateKey: `CustomTeamSharingDemo_${userName}`, userName, initialState: { ...commonInitialState, ...userSpecificInitialState, }, teamSharingOptions: { enableTeamSharing: true, loadSharedEntities: async () => { const data = localStorage.getItem(TEAM_SHARING_STATE_KEY); if (!data) { return []; } try { return JSON.parse(data); } catch (e) { return []; } }, persistSharedEntities: entries => { localStorage.setItem(TEAM_SHARING_STATE_KEY, JSON.stringify(entries)); return Promise.resolve(); }, handleCustomSharedEntityImport: (sharedEntity: CustomSharedEntity) => { if (sharedEntity.Uuid === CUSTOM_SHARE_KEY) { setLocalState(userName, sharedEntity.Entity); } }, }, dashboardOptions: { customToolbars: [ { name: 'CurrentUser', title: 'Current User', toolbarButtons: [ { label: 'Alice', buttonStyle: () => { return { tone: userName === 'Alice' ? 'success' : 'neutral', variant: userName === 'Alice' ? 'raised' : 'outlined', }; }, onClick: () => { updateCurrentUser('Alice'); }, }, { label: 'Bob', buttonStyle: () => { return { tone: userName === 'Bob' ? 'success' : 'neutral', variant: userName === 'Bob' ? 'raised' : 'outlined', }; }, onClick: () => { updateCurrentUser('Bob'); }, }, ], }, { name: 'LocalCustomState', title: 'Local Custom State', toolbarButtons: [ { icon: { name: 'plus', }, buttonStyle: () => { return { tone: 'info', variant: 'outlined', }; }, onClick: () => { mutateLocalState(userName, 1); }, }, { icon: { name: 'minus', }, buttonStyle: () => { return { tone: 'info', variant: 'outlined', }; }, onClick: () => { mutateLocalState(userName, -1); }, }, { label: () => { return `Local value: ${getLocalMutableShare(userName).value}`; }, buttonStyle: () => { return { tone: 'neutral', variant: 'text', className: 'textContent', }; }, disabled: () => true, }, { label: 'Push to Team Sharing', icon: { name: 'upload', }, buttonStyle: () => { return { tone: 'info', variant: 'raised', }; }, onClick: (button, context) => { const currentLocalValue = getLocalMutableShare(userName); context.adaptableApi.teamSharingApi.shareCustomEntity( currentLocalValue, { Uuid: CUSTOM_SHARE_KEY, Name: 'Counter', Description: 'Just a simple example of a shared custom state', } ); }, disabled: (button, context) => { const remoteStateValue = getRemoteCustomSharedEntity(context.adaptableApi)?.Entity ?.value || 0; const localStateValue = getLocalMutableShare(userName).value; return remoteStateValue === localStateValue; }, }, ], }, { name: 'RemoteCustomState', title: 'Remote Custom State', toolbarButtons: [ { label: (button, context) => { const remoteStateEntity = getRemoteCustomSharedEntity( context.adaptableApi )?.Entity; return remoteStateEntity ? `${remoteStateEntity.value} (last updated by ${remoteStateEntity.lastUpdatedBy})` : '<none>'; }, buttonStyle: () => { return { tone: 'neutral', variant: 'text', className: 'textContent', }; }, disabled: () => true, }, { label: 'Import Remote Value', icon: { name: 'import-export', }, buttonStyle: () => { return { tone: 'warning', variant: 'raised', }; }, onClick: (button, context) => { const remoteCustomSharedEntity = getRemoteCustomSharedEntity( context.adaptableApi ); if (remoteCustomSharedEntity) { context.adaptableApi.teamSharingApi.importSharedEntry( remoteCustomSharedEntity ); } }, disabled: (button, context) => { const remoteStateValue = getRemoteCustomSharedEntity( context.adaptableApi )?.Entity?.value; const localStateValue = getLocalMutableShare(userName).value; return ( !remoteStateValue || remoteStateValue === localStateValue ); }, }, ], }, ], customDashboardButtons: [ { tooltip: 'Reset state', icon: { name: 'refresh', }, buttonStyle: { tone: 'error', }, onClick: (button, context) => { localStorage.removeItem(TEAM_SHARING_STATE_KEY); ['Alice', 'Bob'].forEach(user => { localStorage.removeItem( `CustomTeamSharingDemo_${user}_localState` ); localStorage.removeItem(`CustomTeamSharingDemo_${user}`); }); context.adaptableApi.stateApi.reloadInitialState(); }, }, ], }, }; };