Skip to content

Commit

Permalink
feat: add getSelectionRecords plugin bridge method (#1358)
Browse files Browse the repository at this point in the history
* feat: add getSelectionRecords plugin bridge method

* chore: remove useless filter params

* fix: chart plugins filter me
  • Loading branch information
boris-w authored Feb 28, 2025
1 parent c780fb3 commit 492ee8d
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
textPasteHandler,
} from '../utils/copyAndPaste';
import { getSyncCopyData } from '../utils/getSyncCopyData';
import { useSyncSelectionStore } from './useSelectionStore';

export const useSelectionOperation = (props?: {
collapsedGroupIds?: string[];
Expand All @@ -49,6 +50,14 @@ export const useSelectionOperation = (props?: {
const view = useView();
const { searchQuery: search } = useSearch();
const { personalViewCommonQuery } = usePersonalView();
// Parameters for retrieving selected records in plugins
useSyncSelectionStore({
groupBy: view?.group,
personalViewCommonQuery,
collapsedGroupIds,
search,
fields,
});

const { t } = useTranslation(tableConfig.i18nNamespaces);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { IGroup } from '@teable/core';
import type { IGetRecordsRo, IQueryBaseRo } from '@teable/openapi';
import type { IFieldInstance } from '@teable/sdk/model';
import { useEffect } from 'react';
import { create } from 'zustand';

interface ISelectionStore {
fields?: IFieldInstance[];
setFields: (fields: IFieldInstance[] | undefined) => void;
search?: IQueryBaseRo['search'];
setSearch: (search: IQueryBaseRo['search']) => void;
groupBy?: IGroup;
setGroupBy: (groupBy: IGroup | undefined) => void;
personalViewCommonQuery?: IGetRecordsRo;
setPersonalViewCommonQuery: (personalViewCommonQuery: IGetRecordsRo | undefined) => void;
collapsedGroupIds?: string[];
setCollapsedGroupIds: (collapsedGroupIds: string[] | undefined) => void;
}

export const useSelectionStore = create<ISelectionStore>((set) => ({
setGroupBy: (groupBy) => set((state) => ({ ...state, groupBy })),
setPersonalViewCommonQuery: (personalViewCommonQuery) =>
set((state) => ({ ...state, personalViewCommonQuery })),
setCollapsedGroupIds: (collapsedGroupIds) => set((state) => ({ ...state, collapsedGroupIds })),
setSearch: (search) => set((state) => ({ ...state, search })),
setFields: (fields) => set((state) => ({ ...state, fields })),
}));

export const useSyncSelectionStore = ({
groupBy,
personalViewCommonQuery,
collapsedGroupIds,
search,
fields,
}: {
groupBy?: IGroup;
personalViewCommonQuery?: IGetRecordsRo;
collapsedGroupIds?: string[];
search?: IQueryBaseRo['search'];
fields?: IFieldInstance[];
}) => {
useEffect(() => {
useSelectionStore.setState({ groupBy });
}, [groupBy]);
useEffect(() => {
useSelectionStore.setState({ personalViewCommonQuery });
}, [personalViewCommonQuery]);
useEffect(() => {
useSelectionStore.setState({ collapsedGroupIds });
}, [collapsedGroupIds]);
useEffect(() => {
useSelectionStore.setState({ search });
}, [search]);
useEffect(() => {
useSelectionStore.setState({ fields });
}, [fields]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const PluginRender = (props: IPluginRenderProps) => {
updateStorage: (storage) => {
return utilsEvent.updateStorage(storage);
},
getSelectionRecords: (selection, options) => {
return utilsEvent.getSelectionRecords(selection, options);
},
};
const connection = connectToChild<IChildBridgeMethods>({
iframe: iframeRef.current,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
updateDashboardPluginStorage,
updatePluginPanelStorage,
} from '@teable/openapi';
import { useViewId } from '@teable/sdk/hooks';
import type { IParentBridgeUtilsMethods } from '@teable/sdk/plugin-bridge';
import { useEffect, useRef } from 'react';
import type { IPluginParams } from '../types';
import { getSelectionRecords } from './utils/getSelectionRecords';

export const useUtilsEvent = (params: IPluginParams) => {
const tempTokenCacheRef = useRef<IGetTempTokenVo | null>(null);
Expand All @@ -31,12 +33,17 @@ export const useUtilsEvent = (params: IPluginParams) => {
return res.data;
});
},
getSelectionRecords: () => {
console.log('Initializing getSelectionRecords method');
return Promise.resolve({ records: [], fields: [] });
},
});
const { positionId, positionType, pluginId } = params;
const pluginInstallId = 'pluginInstallId' in params ? params.pluginInstallId : undefined;
const shareId = 'shareId' in params ? params.shareId : undefined;
const baseId = 'baseId' in params ? params.baseId : undefined;
const tableId = 'tableId' in params ? params.tableId : undefined;
const viewId = useViewId();

useEffect(() => {
ref.current.updateStorage = (storage) => {
Expand Down Expand Up @@ -68,5 +75,19 @@ export const useUtilsEvent = (params: IPluginParams) => {
};
}, [shareId, pluginId, positionId, tableId, positionType, pluginInstallId, baseId]);

useEffect(() => {
ref.current.getSelectionRecords = (selection, options) => {
if (shareId) {
console.error('Share plugin does not support getSelectionRecords');
return Promise.resolve({ records: [], fields: [] });
}
if (!tableId || !viewId) {
console.error('Table ID or view ID is not available');
return Promise.resolve({ records: [], fields: [] });
}
return getSelectionRecords(tableId, viewId, selection, options);
};
}, [tableId, viewId, shareId]);

return ref.current;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { IRecordsVo } from '@teable/openapi';
import { getRecords } from '@teable/openapi';
import { SelectionRegionType } from '@teable/sdk/components';
import type { IGetSelectionRecordsVo, ISelection } from '@teable/sdk/plugin-bridge';
import { useSelectionStore } from '@/features/app/blocks/view/grid/hooks/useSelectionStore';

export const getSelectionRecords = async (
tableId: string,
viewId: string,
selection: ISelection,
options?: {
skip?: number;
take?: number;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
): Promise<IGetSelectionRecordsVo> => {
const { groupBy, search, fields, collapsedGroupIds, personalViewCommonQuery } =
useSelectionStore.getState();
if (!fields) {
return {
records: [],
fields: [],
};
}

const { type, range } = selection;
const { skip = 0, take = 100 } = options || {};

const getRecordsByQuery = ({
skip,
take,
projection,
}: {
skip: number;
take: number;
projection: string[];
}) => {
return getRecords(tableId, {
...personalViewCommonQuery,
viewId,
groupBy,
search,
collapsedGroupIds,
projection,
skip,
take,
}).then((res) => res.data.records);
};
switch (type) {
case SelectionRegionType.Cells: {
const [[startColIndex, startRowIndex], [endColIndex, endRowIndex]] = range;
if (
startColIndex == null ||
startRowIndex == null ||
endColIndex == null ||
endRowIndex == null
) {
throw new Error('Invalid selection range');
}
const projectionFields = fields.slice(startColIndex, endColIndex + 1);
const projection = projectionFields.map((item) => item.name);
const records = await getRecordsByQuery({
projection,
skip: skip + startRowIndex,
take: Math.min(take, endRowIndex - startRowIndex + 1),
});
return {
records,
fields: projectionFields.map((item) => item['doc'].data),
};
}
case SelectionRegionType.Rows: {
const allRecords: IRecordsVo['records'] = [];
let totalSkip = skip;
let totalTake = take;
const projection = fields.map((item) => item.name);
for (let i = 0; i < range.length; i++) {
const [startRowIndex, endRowIndex] = range[i];
const currentRowCount = endRowIndex - startRowIndex + 1;
if (totalSkip >= currentRowCount) {
totalSkip -= currentRowCount;
continue;
}

const records = await getRecordsByQuery({
projection,
skip: totalSkip + startRowIndex,
take: Math.min(totalTake, currentRowCount),
});
allRecords.push(...records);
if (totalTake > currentRowCount) {
totalTake -= currentRowCount;
totalSkip = 0;
} else {
break;
}
}
return {
records: allRecords,
fields: fields.map((item) => item['doc'].data),
};
}
case SelectionRegionType.Columns: {
const projections: string[] = [];
for (let i = 0; i < fields.length; i++) {
const [startColIndex, endColIndex] = range[i];
projections.push(...fields.slice(startColIndex, endColIndex).map((item) => item.name));
}
const records = await getRecordsByQuery({
projection: projections,
skip,
take,
});
return {
records,
fields: fields.map((item) => item['doc'].data),
};
}
default: {
console.error(`Unsupported selection type: ${type}`);
return {
records: [],
fields: [],
};
}
}
};
17 changes: 16 additions & 1 deletion packages/sdk/src/plugin-bridge/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IGetBasePermissionVo, IGetTempTokenVo } from '@teable/openapi';
import type { IFieldVo } from '@teable/core';
import type { IGetBasePermissionVo, IGetTempTokenVo, IRecordsVo } from '@teable/openapi';
import type { AsyncMethodReturns } from 'penpal';
import type { IRange, SelectionRegionType } from '../components/grid/interface';

Expand All @@ -17,6 +18,11 @@ export interface ISelection {
type: SelectionRegionType;
}

export interface IGetSelectionRecordsVo {
records: IRecordsVo['records'];
fields: IFieldVo[];
}

export type IBasePermissions = IGetBasePermissionVo;

export interface IParentBridgeUIMethods {
Expand All @@ -28,6 +34,15 @@ export interface IParentBridgeUtilsMethods {
updateStorage: (storage?: Record<string, unknown>) => Promise<Record<string, unknown>>;
getAuthCode: () => Promise<string>;
getSelfTempToken: () => Promise<IGetTempTokenVo>;
getSelectionRecords: (
selection: ISelection,
options?: {
// The number of records to skip, default is 0
skip?: number;
// The number of records to take, default is 100
take?: number;
}
) => Promise<IGetSelectionRecordsVo>;
}

export type IParentBridgeMethods = IParentBridgeUIMethods & IParentBridgeUtilsMethods;
Expand Down
5 changes: 5 additions & 0 deletions plugins/src/app/chart/hooks/useBaseQueryData.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { type CellFormat } from '@teable/core';
import { useSession } from '@teable/sdk';
import { useContext } from 'react';
import { useEnv } from '../../../hooks/useEnv';
import { ChartContext } from '../components/ChartProvider';
Expand All @@ -9,6 +10,7 @@ export const useBaseQueryData = (cellFormat?: CellFormat) => {
const { baseId, pluginId } = useEnv();
const { storage, onQueryError } = useContext(ChartContext);
const query = storage?.query;
const { user } = useSession();

const { data } = useQuery({
queryKey: baseQueryKeys(baseId, query!, cellFormat),
Expand All @@ -18,6 +20,9 @@ export const useBaseQueryData = (cellFormat?: CellFormat) => {
pluginId,
queryKeys: queryKey,
onQueryError,
options: {
currentUserId: user?.id,
},
});
},
});
Expand Down
12 changes: 10 additions & 2 deletions plugins/src/app/chart/query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CellFormat, HttpError } from '@teable/core';
import { Me, type CellFormat, type HttpError } from '@teable/core';
import { BASE_QUERY, urlBuilder } from '@teable/openapi';
import type { IBaseQuery, IBaseQueryVo } from '@teable/openapi';
import { fetchGetToken, GetTokenType } from '../../api';
Expand Down Expand Up @@ -35,16 +35,24 @@ export const getBaseQueryData = async ({
pluginId,
queryKeys,
onQueryError,
options,
}: {
pluginId: string;
queryKeys: ReturnType<typeof baseQueryKeys>;
onQueryError?: (error: string | undefined) => void;
options?: {
currentUserId?: string;
};
}) => {
onQueryError?.(undefined);
const [, baseId, query, cellFormat] = queryKeys;
const url = urlBuilder(BASE_QUERY, { baseId });
let queryString = JSON.stringify(query);
if (options?.currentUserId) {
queryString = queryString.replaceAll(`"value":"${Me}"`, `"value":"${options.currentUserId}"`);
}
const params = new URLSearchParams({
query: JSON.stringify(query),
query: queryString,
});
if (cellFormat) {
params.append('cellFormat', cellFormat);
Expand Down

0 comments on commit 492ee8d

Please sign in to comment.