@adaptabletools/adaptable-plugin-ipushpull 22.0.1 → 22.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
  import ObjectFactory from '@adaptabletools/adaptable/src/Utilities/ObjectFactory';
5
5
  import { StringExtensions } from '@adaptabletools/adaptable/src/Utilities/Extensions/StringExtensions';
6
6
  import { Flex } from '@adaptabletools/adaptable/src/components/Flex';
7
- import DropdownButton from '@adaptabletools/adaptable/src/components/DropdownButton';
7
+ import { Select } from '@adaptabletools/adaptable/src/components/Select';
8
8
  import { ButtonExport } from '@adaptabletools/adaptable/src/View/Components/Buttons/ButtonExport';
9
9
  import { ButtonPause } from '@adaptabletools/adaptable/src/View/Components/Buttons/ButtonPause';
10
10
  import { ButtonPlay } from '@adaptabletools/adaptable/src/View/Components/Buttons/ButtonPlay';
@@ -62,58 +62,42 @@ const IPushPullViewPanelComponent = (props) => {
62
62
  let allReports = systemReports
63
63
  .filter((s) => props.api.exportApi.internalApi.isSystemReportActive(s))
64
64
  .concat(props.Reports.map((r) => r.Name));
65
- let availableReports = allReports.map((report) => {
66
- return {
67
- label: report,
68
- value: report,
69
- onClick: () => onSelectedReportChanged(report),
70
- };
71
- });
72
- let availableFolders = props.IPushPullDomainsPages?.map((iPushPullDomain) => {
73
- return {
74
- label: iPushPullDomain.Name,
75
- value: iPushPullDomain.Name,
76
- onClick: () => onFolderChanged(iPushPullDomain.Name),
77
- };
78
- }) ?? [];
79
- let availablePages = props.CurrentIPushPullAvailablePages?.map((page) => {
80
- return {
81
- label: page,
82
- value: page,
83
- onClick: () => onPageChanged(page),
84
- };
85
- }) ?? [];
86
- // this is clearly ridiculous!
87
- // im getting tired...
88
- let isCompletedReport = StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullReportName) &&
89
- StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullFolder) &&
90
- StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullPage);
65
+ let availableReports = allReports.map((report) => ({
66
+ label: report,
67
+ value: report,
68
+ }));
69
+ let availableFolders = props.IPushPullDomainsPages?.map((iPushPullDomain) => ({
70
+ label: iPushPullDomain.Name,
71
+ value: iPushPullDomain.Name,
72
+ })) ?? [];
73
+ let availablePages = props.CurrentIPushPullAvailablePages?.map((page) => ({
74
+ label: page,
75
+ value: page,
76
+ })) ?? [];
77
+ let hasReport = StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullReportName);
78
+ let hasFolder = StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullFolder);
79
+ let hasPage = StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullPage);
80
+ let isCompletedReport = hasReport && hasFolder && hasPage;
91
81
  let isLiveIPushPullReport = isCompletedReport &&
92
82
  props.CurrentLiveIPushPullReport &&
93
83
  props.CurrentIPushPullReportName == props.CurrentLiveIPushPullReport.ReportName &&
94
84
  props.CurrentIPushPullFolder == props.CurrentLiveIPushPullReport.Folder &&
95
85
  props.CurrentIPushPullPage == props.CurrentLiveIPushPullReport.Page;
96
86
  const elementType = props.viewType === 'Toolbar' ? 'DashboardToolbar' : 'ToolPanel';
97
- return props.IsIPushPullRunning ? (React.createElement(Flex, { flexDirection: "row", className: `ab-${elementType}__IPushPull__wrap`, flexWrap: props.viewType === 'ToolPanel' ? 'wrap' : 'nowrap' },
98
- React.createElement(Flex, null,
99
- React.createElement(DropdownButton, { disabled: allReports.length == 0 || isLiveIPushPullReport, items: availableReports, columns: ['label'], className: `ab-${elementType}__IPushPull__select twa:min-w-[140px] twa:text-0 twa:mr-2`, onClear: () => onSelectedReportChanged(''), showClearButton: StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullReportName), variant: "outlined" }, StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullReportName)
100
- ? props.CurrentIPushPullReportName
101
- : 'Select Report')),
102
- React.createElement(Flex, null,
103
- React.createElement(DropdownButton, { disabled: allReports.length == 0 || isLiveIPushPullReport, items: availableFolders, columns: ['label'], className: `ab-${elementType}__IPushPull__select twa:min-w-[140px] twa:text-0 twa:mr-2`, onClear: () => onFolderChanged(''), showClearButton: StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullFolder), variant: "outlined" }, StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullFolder)
104
- ? props.CurrentIPushPullFolder
105
- : 'Select Folder')),
106
- React.createElement(Flex, null,
107
- React.createElement(DropdownButton, { disabled: allReports.length == 0 || isLiveIPushPullReport, items: availablePages, columns: ['label'], className: `ab-${elementType}__IPushPull__select twa:min-w-[140px] twa:text-0 twa:mr-2`, onClear: () => onPageChanged(''), showClearButton: StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullPage), variant: "outlined" }, StringExtensions.IsNotNullOrEmpty(props.CurrentIPushPullPage)
108
- ? props.CurrentIPushPullPage
109
- : 'Select Page')),
110
- React.createElement(Flex, { className: "twa:min-w-[148px]" },
111
- React.createElement(ButtonExport, { className: `ab-${elementType}__IPushPull__export twa:ml-1`, onClick: () => onIPushPullSendSnapshot(), tooltip: "Send Snapshot to ipushpull", disabled: isLiveIPushPullReport || !isCompletedReport, accessLevel: props.accessLevel }),
112
- true ? (React.createElement(ButtonPause, { className: `ab-${elementType}__IPushPull__pause twa:ml-1 twa:fill-red-500`, onClick: () => props.onIPushPullStopLiveData(), tooltip: "Stop sync with ipushpull", disabled: !isLiveIPushPullReport, accessLevel: props.accessLevel })) : (React.createElement(ButtonPlay, { className: `ab-${elementType}__IPushPull__play twa:ml-1`, onClick: () => onIPushPullStartLiveData(), tooltip: "Start sync with ipushpull", disabled: isLiveIPushPullReport || !isCompletedReport, accessLevel: props.accessLevel })),
113
- isCompletedReport && (React.createElement(Flex, { className: join(props.accessLevel == 'ReadOnly' ? GeneralConstants.READ_ONLY_STYLE : '', `ab-${elementType}__IPushPull__controls`), alignItems: "stretch" }, props.api.entitlementApi.isModuleFullEntitlement('Schedule') && (React.createElement(ButtonSchedule, { className: `ab-${elementType}__IPushPull__schedule twa:ml-1`, onClick: () => onNewIPushPullSchedule(), tooltip: "Schedule", disabled: isLiveIPushPullReport || !isCompletedReport, accessLevel: props.accessLevel })))),
114
- ' ',
115
- React.createElement(ButtonNewPage, { className: `ab-${elementType}__IPushPull__newpage twa:ml-1`, onClick: () => props.onShowAddIPushPullPage(), tooltip: "New Page", disabled: isLiveIPushPullReport, accessLevel: props.accessLevel }),
116
- React.createElement(ButtonLogout, { className: `ab-${elementType}__IPushPull__logout twa:ml-1`, onClick: () => getIPPApi().logoutFromIPushPull(), tooltip: "Logout", disabled: isLiveIPushPullReport, accessLevel: props.accessLevel })))) : (React.createElement(ButtonLogin, { className: `ab-${elementType}__IPushPull__login twa:ml-1`, onClick: () => props.onShowIPushPullLogin(), tooltip: "Login to ipushpull", accessLevel: props.accessLevel },
87
+ return props.IsIPushPullRunning ? (React.createElement(Flex, { flexDirection: "row", className: `ab-${elementType}__IPushPull__wrap twa:gap-1`, flexWrap: props.viewType === 'ToolPanel' ? 'wrap' : 'nowrap' },
88
+ React.createElement(Flex, { className: "twa:min-w-[140px]" },
89
+ React.createElement(Select, { disabled: allReports.length == 0 || isLiveIPushPullReport, options: availableReports, className: `ab-${elementType}__IPushPull__select twa:w-full`, placeholder: "Select Report", onChange: (reportName) => onSelectedReportChanged(reportName), value: props.CurrentIPushPullReportName, isClearable: true })),
90
+ React.createElement(Flex, { className: "twa:min-w-[140px]" },
91
+ React.createElement(Select, { disabled: !hasReport || isLiveIPushPullReport, options: availableFolders, className: `ab-${elementType}__IPushPull__select twa:w-full`, placeholder: "Select Folder", onChange: (folder) => onFolderChanged(folder), value: props.CurrentIPushPullFolder, isClearable: true })),
92
+ React.createElement(Flex, { className: "twa:min-w-[140px]" },
93
+ React.createElement(Select, { disabled: !hasFolder || isLiveIPushPullReport, options: availablePages, className: `ab-${elementType}__IPushPull__select twa:w-full`, placeholder: "Select Page", onChange: (page) => onPageChanged(page), value: props.CurrentIPushPullPage, isClearable: true })),
94
+ React.createElement(Flex, { className: join(props.accessLevel == 'ReadOnly' ? GeneralConstants.READ_ONLY_STYLE : '', `ab-${elementType}__IPushPull__controls twa:w-full`) },
95
+ React.createElement(Flex, null,
96
+ React.createElement(ButtonExport, { className: `ab-${elementType}__IPushPull__export`, onClick: () => onIPushPullSendSnapshot(), tooltip: "Send Snapshot to ipushpull", disabled: isLiveIPushPullReport || !isCompletedReport, accessLevel: props.accessLevel }),
97
+ isLiveIPushPullReport ? (React.createElement(ButtonPause, { className: `ab-${elementType}__IPushPull__pause twa:fill-red-500`, onClick: () => props.onIPushPullStopLiveData(), tooltip: "Stop sync with ipushpull", disabled: !isLiveIPushPullReport, accessLevel: props.accessLevel })) : (React.createElement(ButtonPlay, { className: `ab-${elementType}__IPushPull__play`, onClick: () => onIPushPullStartLiveData(), tooltip: "Start sync with ipushpull", disabled: isLiveIPushPullReport || !isCompletedReport, accessLevel: props.accessLevel })),
98
+ props.api.entitlementApi.isModuleFullEntitlement('Schedule') && (React.createElement(ButtonSchedule, { className: `ab-${elementType}__IPushPull__schedule`, onClick: () => onNewIPushPullSchedule(), tooltip: "Schedule", disabled: isLiveIPushPullReport || !isCompletedReport, accessLevel: props.accessLevel })),
99
+ React.createElement(ButtonNewPage, { className: `ab-${elementType}__IPushPull__newpage`, onClick: () => props.onShowAddIPushPullPage(), tooltip: "New Page", disabled: !hasFolder || isLiveIPushPullReport, accessLevel: props.accessLevel }),
100
+ React.createElement(ButtonLogout, { className: `ab-${elementType}__IPushPull__logout`, onClick: () => getIPPApi().logoutFromIPushPull(), tooltip: "Logout", disabled: isLiveIPushPullReport, accessLevel: props.accessLevel }))))) : (React.createElement(ButtonLogin, { className: `ab-${elementType}__IPushPull__login twa:ml-1`, onClick: () => props.onShowIPushPullLogin(), tooltip: "Login to ipushpull", accessLevel: props.accessLevel },
117
101
  ' ',
118
102
  "Login"));
119
103
  };
package/src/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { AdaptablePlugin } from '@adaptabletools/adaptable';
1
+ import { AdaptablePlugin } from '@adaptabletools/adaptable/types';
2
2
  import type { Middleware } from 'redux';
3
3
  import * as Redux from 'redux';
4
4
  import { IPushPullState } from '@adaptabletools/adaptable/src/AdaptableState/IPushPullState';
5
5
  import { IPushPullApi } from '@adaptabletools/adaptable/src/Api/IPushPullApi';
6
- import { IPushPullPluginOptions } from '@adaptabletools/adaptable/src/AdaptableOptions/IPushPullPluginOptions';
6
+ import { IPushPullPluginOptions } from './IPushPullPluginOptions';
7
7
  import { IAdaptable } from '@adaptabletools/adaptable/src/AdaptableInterfaces/IAdaptable';
8
8
  declare class IPushPullPlugin extends AdaptablePlugin {
9
9
  options: IPushPullPluginOptions;
@@ -13,10 +13,11 @@ declare class IPushPullPlugin extends AdaptablePlugin {
13
13
  constructor(options?: IPushPullPluginOptions);
14
14
  afterInitApi(adaptable: IAdaptable): void;
15
15
  rootReducer: (rootReducer: any) => {
16
- System: (state: IPushPullState, action: Redux.Action) => IPushPullState;
16
+ Internal: (state: IPushPullState, action: Redux.Action) => IPushPullState;
17
17
  };
18
18
  reduxMiddleware: (adaptable: IAdaptable) => Middleware;
19
19
  afterInitStore(adaptable: IAdaptable): void;
20
20
  }
21
- declare const _default: (options?: IPushPullPluginOptions) => IPushPullPlugin;
22
- export default _default;
21
+ export type { IPushPullPluginOptions, IPushPullConfig } from './IPushPullPluginOptions';
22
+ export declare const ipushpullPlugin: (options?: IPushPullPluginOptions) => IPushPullPlugin;
23
+ export default ipushpullPlugin;
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AdaptablePlugin } from '@adaptabletools/adaptable';
1
+ import { AdaptablePlugin } from '@adaptabletools/adaptable/types';
2
2
  import env from './env';
3
3
  import packageJson from '../package.json';
4
4
  import adaptableCorePackageJson from '@adaptabletools/adaptable/package.json';
@@ -8,8 +8,8 @@ import * as PopupRedux from '@adaptabletools/adaptable/src/Redux/ActionsReducers
8
8
  import { AdaptableViewFactory, AdaptableViewPanelFactory, } from '@adaptabletools/adaptable/src/View/AdaptableViewFactory';
9
9
  import { PushPullModule } from './Module/PushPullModule';
10
10
  import { IPushPullApiImpl } from './IPushPullApiImpl';
11
- import { PushPullService } from './Utilities/Services/PushPullService';
12
- import ipushpull from 'ipushpull-js';
11
+ import { IPushPullService } from './Utilities/Services/IPushPullService';
12
+ import { IPushPullClient } from './ipushpull-client';
13
13
  import { iPushPullInitialState, IPushPullReducer, IPushPullSetCurrentAvailablePages, IPushPullSetCurrentPage, } from './Redux/ActionReducers/IPushPullRedux';
14
14
  import { IPushPullLoginPopup } from './View/IPushPullLoginPopup';
15
15
  import { IPushPullAddPagePopup } from './View/IPushPullAddPagePopup';
@@ -22,25 +22,18 @@ const suffix = name.endsWith('-cjs') ? '-cjs' : '';
22
22
  if (version !== coreVersion) {
23
23
  console.warn(`Version mismatch: "@adaptabletools/adaptable-plugin-ipushpull${suffix}" (v${version}) and "@adaptabletools/adaptable${suffix}" (v${coreVersion}) have different versions. They should be the exact same version.`);
24
24
  }
25
+ const DEFAULT_API_URL = 'https://test.ipushpull.com/api/1.0';
25
26
  const getApiKey = () => {
26
- let key = env.IPUSHPULL_API_KEY; // need to make sure that is always there
27
- return key;
27
+ return env.IPUSHPULL_API_KEY || '';
28
28
  };
29
29
  const getApiSecret = () => {
30
- let secret = env.IPUSHPULL_API_SECRET; // need to make sure that is always there
31
- return secret;
30
+ return env.IPUSHPULL_API_SECRET || '';
32
31
  };
33
32
  const defaultOptions = {
34
33
  ippConfig: {
35
- api_secret: getApiSecret() || '',
36
- api_key: getApiKey() || '',
37
- api_url: 'https://www.ipushpull.com/api/1.0',
38
- ws_url: 'https://www.ipushpull.com',
39
- web_url: 'https://www.ipushpull.com',
40
- docs_url: 'https://docs.ipushpull.com',
41
- storage_prefix: 'ipp_local',
42
- transport: 'polling',
43
- hsts: false, // strict cors policy
34
+ api_key: getApiKey(),
35
+ api_secret: getApiSecret(),
36
+ api_url: DEFAULT_API_URL,
44
37
  },
45
38
  autoLogin: false,
46
39
  throttleTime: 2000,
@@ -52,40 +45,37 @@ class IPushPullPlugin extends AdaptablePlugin {
52
45
  PushPullService;
53
46
  constructor(options) {
54
47
  super(options);
48
+ const defaults = defaultOptions.ippConfig;
49
+ const userConfig = options?.ippConfig;
55
50
  const ippConfig = {
56
- ...defaultOptions.ippConfig,
57
- ...(options || {}).ippConfig,
51
+ api_url: userConfig?.api_url ?? defaults.api_url ?? DEFAULT_API_URL,
52
+ api_key: userConfig?.api_key || defaults.api_key,
53
+ api_secret: userConfig?.api_secret || defaults.api_secret,
58
54
  };
59
- if (!ippConfig.api_key) {
60
- ippConfig.api_key = defaultOptions.ippConfig.api_key;
61
- }
62
- if (!ippConfig.api_secret) {
63
- ippConfig.api_secret = defaultOptions.ippConfig.api_secret;
64
- }
65
55
  this.options = {
66
56
  ...defaultOptions,
67
57
  ...options,
68
58
  ippConfig,
69
59
  };
70
- /**
71
- * Contains the objects required to export (snapshot or live) data to ipushpull from AdapTable.
72
- *
73
- * Includes ipushpull config and objects and, optionally, any ipushpull Reports (including schedules).
74
- */
75
- // IPushPull?: IPushPullState;
76
- ipushpull.config.set(this.options.ippConfig);
77
60
  }
78
61
  afterInitApi(adaptable) {
62
+ const config = this.options.ippConfig;
63
+ const client = new IPushPullClient({
64
+ api_url: config.api_url ?? DEFAULT_API_URL,
65
+ api_key: config.api_key,
66
+ api_secret: config.api_secret,
67
+ });
79
68
  this.iPushPullApi = new IPushPullApiImpl(adaptable, this.options);
80
- this.PushPullService = new PushPullService(adaptable);
81
- this.iPushPullApi.setIPushPullInstance(ipushpull);
69
+ this.PushPullService = new IPushPullService(adaptable, this.options.cellStyles);
70
+ this.iPushPullApi.setIPushPullInstance(client);
82
71
  this.registerProperty('api', () => this.iPushPullApi);
83
72
  this.registerProperty('service', () => this.PushPullService);
84
73
  }
74
+ // FIXME AFL improve typing
85
75
  rootReducer = (rootReducer) => {
86
76
  return {
87
- System: (state, action) => {
88
- let augmentedState = rootReducer.System(state, action);
77
+ Internal: (state, action) => {
78
+ let augmentedState = rootReducer.Internal(state, action);
89
79
  if (!state) {
90
80
  // required for store initialization
91
81
  // (idiomatic way of default parameter value in reducer is not feasible because the passed argument is already initialized by the System reducer)
@@ -142,7 +132,7 @@ class IPushPullPlugin extends AdaptablePlugin {
142
132
  return next(action);
143
133
  }
144
134
  case IPUSHPULL_DOMAIN_PAGES_SET: {
145
- //refresh the available pages
135
+ const result = next(action);
146
136
  const currentFolder = middlewareAPI.getState().Internal.IPushPullCurrentFolder;
147
137
  const isFolderValid = StringExtensions.IsNotNullOrEmpty(currentFolder);
148
138
  if (isFolderValid) {
@@ -151,7 +141,7 @@ class IPushPullPlugin extends AdaptablePlugin {
151
141
  .getPagesForIPushPullDomain(currentFolder);
152
142
  middlewareAPI.dispatch(IPushPullSetCurrentAvailablePages(availablePages));
153
143
  }
154
- return next(action);
144
+ return result;
155
145
  }
156
146
  default: {
157
147
  return next(action);
@@ -167,4 +157,5 @@ class IPushPullPlugin extends AdaptablePlugin {
167
157
  AdaptableViewPanelFactory.set(IPushPullModuleId, IPushPullViewPanelControl);
168
158
  }
169
159
  }
170
- export default (options) => new IPushPullPlugin(options);
160
+ export const ipushpullPlugin = (options) => new IPushPullPlugin(options);
161
+ export default ipushpullPlugin;
@@ -0,0 +1,28 @@
1
+ import { IPushPullClientConfig, IPushPullTokens, IPushPullDomainAccessInfo, IPushPullPageContentPayload, IPushPullPageContentResponse, IPushPullCellStyle } from './types';
2
+ export declare class IPushPullClient {
3
+ private config;
4
+ private tokens;
5
+ private refreshPromise;
6
+ constructor(config: IPushPullClientConfig);
7
+ private getBasicAuthHeader;
8
+ private buildOAuthBody;
9
+ login(username: string, password: string): Promise<IPushPullTokens>;
10
+ refreshToken(): Promise<IPushPullTokens>;
11
+ private doRefreshToken;
12
+ logout(): Promise<void>;
13
+ getDomainsAndPages(): Promise<IPushPullDomainAccessInfo[]>;
14
+ getPageContent(folderId: number, pageId: number): Promise<any>;
15
+ updatePageContent(folderId: number, pageId: number, payload: IPushPullPageContentPayload): Promise<IPushPullPageContentResponse>;
16
+ createPage(folderId: number, pageName: string, description?: string): Promise<any>;
17
+ isAuthenticated(): boolean;
18
+ private fetchWithAuth;
19
+ }
20
+ /**
21
+ * Converts per-cell data (array of rows, each cell being { value, formatted_value, style })
22
+ * into the official ipushpull page content format with deduplicated styles.
23
+ */
24
+ export declare function buildPageContentPayload(cellData: Array<Array<{
25
+ value: any;
26
+ formatted_value: any;
27
+ style: IPushPullCellStyle;
28
+ }>>): IPushPullPageContentPayload;
@@ -0,0 +1,198 @@
1
+ export class IPushPullClient {
2
+ config;
3
+ tokens = null;
4
+ refreshPromise = null;
5
+ constructor(config) {
6
+ this.config = config;
7
+ }
8
+ getBasicAuthHeader() {
9
+ const credentials = `${this.config.api_key}:${this.config.api_secret}`;
10
+ return `Basic ${btoa(credentials)}`;
11
+ }
12
+ buildOAuthBody(params) {
13
+ return Object.entries(params)
14
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
15
+ .join('&');
16
+ }
17
+ async login(username, password) {
18
+ const url = `${this.config.api_url}/oauth/token/`;
19
+ const body = this.buildOAuthBody({
20
+ grant_type: 'password',
21
+ username,
22
+ password,
23
+ });
24
+ const response = await fetch(url, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/x-www-form-urlencoded',
28
+ Authorization: this.getBasicAuthHeader(),
29
+ },
30
+ body,
31
+ });
32
+ if (!response.ok) {
33
+ const errorData = await response.json().catch(() => ({}));
34
+ const message = errorData.error_description || errorData.error || response.statusText;
35
+ const err = new Error(message);
36
+ err.data = errorData;
37
+ throw err;
38
+ }
39
+ this.tokens = await response.json();
40
+ return this.tokens;
41
+ }
42
+ async refreshToken() {
43
+ if (!this.tokens?.refresh_token) {
44
+ throw new Error('No refresh token available. Please login first.');
45
+ }
46
+ // Deduplicate concurrent refresh attempts
47
+ if (this.refreshPromise) {
48
+ return this.refreshPromise;
49
+ }
50
+ this.refreshPromise = this.doRefreshToken();
51
+ try {
52
+ const result = await this.refreshPromise;
53
+ return result;
54
+ }
55
+ finally {
56
+ this.refreshPromise = null;
57
+ }
58
+ }
59
+ async doRefreshToken() {
60
+ const url = `${this.config.api_url}/oauth/token/`;
61
+ const body = this.buildOAuthBody({
62
+ grant_type: 'refresh_token',
63
+ refresh_token: this.tokens.refresh_token,
64
+ });
65
+ const response = await fetch(url, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/x-www-form-urlencoded',
69
+ Authorization: this.getBasicAuthHeader(),
70
+ },
71
+ body,
72
+ });
73
+ if (!response.ok) {
74
+ this.tokens = null;
75
+ const errorData = await response.json().catch(() => ({}));
76
+ throw new Error(errorData.error_description || errorData.error || 'Token refresh failed');
77
+ }
78
+ this.tokens = await response.json();
79
+ return this.tokens;
80
+ }
81
+ async logout() {
82
+ if (!this.tokens?.access_token) {
83
+ return;
84
+ }
85
+ const url = `${this.config.api_url}/oauth/logout/`;
86
+ await fetch(url, {
87
+ method: 'POST',
88
+ headers: {
89
+ Authorization: `Bearer ${this.tokens.access_token}`,
90
+ },
91
+ });
92
+ this.tokens = null;
93
+ }
94
+ async getDomainsAndPages() {
95
+ const url = `${this.config.api_url}/domain_page_access`;
96
+ const response = await this.fetchWithAuth(url);
97
+ const data = await response.json();
98
+ return (data.domains ?? data).map((domain) => ({
99
+ id: domain.id,
100
+ name: domain.name,
101
+ pages: (domain.current_user_domain_page_access?.pages ?? domain.pages ?? []).map((page) => ({
102
+ id: page.id,
103
+ name: page.name,
104
+ special_page_type: page.special_page_type ?? 0,
105
+ write_access: page.write_access ?? false,
106
+ })),
107
+ }));
108
+ }
109
+ async getPageContent(folderId, pageId) {
110
+ const baseUrl = this.config.api_url.replace(/\/api\/1\.0\/?$/, '');
111
+ const url = `${baseUrl}/api/2.0/domains/id/${folderId}/page_content/id/${pageId}/`;
112
+ const response = await this.fetchWithAuth(url);
113
+ return response.json();
114
+ }
115
+ async updatePageContent(folderId, pageId, payload) {
116
+ const baseUrl = this.config.api_url.replace(/\/api\/1\.0\/?$/, '');
117
+ const url = `${baseUrl}/api/2.0/domains/id/${folderId}/page_content/id/${pageId}/`;
118
+ const response = await this.fetchWithAuth(url, {
119
+ method: 'PUT',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify(payload),
122
+ });
123
+ return response.json();
124
+ }
125
+ async createPage(folderId, pageName, description) {
126
+ const url = `${this.config.api_url}/domains/${folderId}/pages/`;
127
+ const response = await this.fetchWithAuth(url, {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify({
131
+ name: pageName,
132
+ description: description ?? '',
133
+ }),
134
+ });
135
+ return response.json();
136
+ }
137
+ isAuthenticated() {
138
+ return this.tokens != null;
139
+ }
140
+ async fetchWithAuth(url, options = {}) {
141
+ if (!this.tokens?.access_token) {
142
+ throw new Error('Not authenticated. Please login first.');
143
+ }
144
+ const headers = new Headers(options.headers);
145
+ headers.set('Authorization', `Bearer ${this.tokens.access_token}`);
146
+ let response = await fetch(url, { ...options, headers });
147
+ if (response.status === 401) {
148
+ await this.refreshToken();
149
+ headers.set('Authorization', `Bearer ${this.tokens.access_token}`);
150
+ response = await fetch(url, { ...options, headers });
151
+ }
152
+ if (!response.ok) {
153
+ const errorData = await response.json().catch(() => ({}));
154
+ const message = errorData.detail || errorData.error || response.statusText;
155
+ throw new Error(message);
156
+ }
157
+ return response;
158
+ }
159
+ }
160
+ /**
161
+ * Converts per-cell data (array of rows, each cell being { value, formatted_value, style })
162
+ * into the official ipushpull page content format with deduplicated styles.
163
+ */
164
+ export function buildPageContentPayload(cellData) {
165
+ const values = [];
166
+ const formattedValues = [];
167
+ const uniqueStyles = [];
168
+ const cellStyles = [];
169
+ const styleIndex = new Map();
170
+ for (const row of cellData) {
171
+ const rowValues = [];
172
+ const rowFormattedValues = [];
173
+ const rowStyleIndices = [];
174
+ for (const cell of row) {
175
+ rowValues.push(cell.value);
176
+ rowFormattedValues.push(cell.formatted_value);
177
+ const styleKey = JSON.stringify(cell.style);
178
+ let idx = styleIndex.get(styleKey);
179
+ if (idx === undefined) {
180
+ idx = uniqueStyles.length;
181
+ uniqueStyles.push(cell.style);
182
+ styleIndex.set(styleKey, idx);
183
+ }
184
+ rowStyleIndices.push(idx);
185
+ }
186
+ values.push(rowValues);
187
+ formattedValues.push(rowFormattedValues);
188
+ cellStyles.push(rowStyleIndices);
189
+ }
190
+ return {
191
+ content: {
192
+ values,
193
+ formatted_values: formattedValues,
194
+ unique_styles: uniqueStyles,
195
+ cell_styles: cellStyles,
196
+ },
197
+ };
198
+ }
@@ -0,0 +1,4 @@
1
+ export { IPushPullClient, buildPageContentPayload } from './IPushPullClient';
2
+ export { getThemeStyles } from './themes';
3
+ export type { IPushPullTheme, IPushPullThemeStyles } from './themes';
4
+ export type { IPushPullClientConfig, IPushPullTokens, IPushPullPageAccessInfo, IPushPullDomainAccessInfo, IPushPullCellStyle, IPushPullPageContentPayload, IPushPullPageContentResponse, } from './types';
@@ -0,0 +1,2 @@
1
+ export { IPushPullClient, buildPageContentPayload } from './IPushPullClient';
2
+ export { getThemeStyles } from './themes';
@@ -0,0 +1,8 @@
1
+ import { IPushPullCellStyle } from './types';
2
+ export type IPushPullTheme = 'lightTheme' | 'darkTheme';
3
+ export interface IPushPullThemeStyles {
4
+ headerStyle: IPushPullCellStyle;
5
+ rowStyle: IPushPullCellStyle;
6
+ altRowStyle: IPushPullCellStyle;
7
+ }
8
+ export declare function getThemeStyles(theme: IPushPullTheme): IPushPullThemeStyles;