feat: export plugin (#73)
* feat: add data export action in table view * feat: add data render based on interface type for exporting * fix: use rewrite action name for permissions * feat: add support for subTable type field * add datetime render * export filter support selectedRowKeys * docs Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
		
							parent
							
								
									6081ee29af
								
							
						
					
					
						commit
						cb8edc7b75
					
				
							
								
								
									
										27
									
								
								docs/plugins/packages/export.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								docs/plugins/packages/export.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
---
 | 
			
		||||
title: '@nocobase/plugin-export'
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# @nocobase/plugin-export
 | 
			
		||||
 | 
			
		||||
提供导出功能
 | 
			
		||||
 | 
			
		||||
<Alert title="注意" type="warning">
 | 
			
		||||
暂时只支持 excel 导出
 | 
			
		||||
</Alert>
 | 
			
		||||
 | 
			
		||||
## 安装
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
yarn nocobase pull export --start
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Action API
 | 
			
		||||
 | 
			
		||||
### export
 | 
			
		||||
 | 
			
		||||
参数和 list 一致,暂时只支持 excel 导出
 | 
			
		||||
 | 
			
		||||
```ts
 | 
			
		||||
api.resource(resourceName).export(params);
 | 
			
		||||
```
 | 
			
		||||
@ -16,6 +16,7 @@
 | 
			
		||||
    "@nocobase/plugin-automations": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/plugin-china-region": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/plugin-collections": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/plugin-export": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/plugin-file-manager": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/plugin-pages": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/plugin-permissions": "^0.4.0-alpha.7",
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ const plugins = [
 | 
			
		||||
  '@nocobase/plugin-permissions',
 | 
			
		||||
  '@nocobase/plugin-automations',
 | 
			
		||||
  '@nocobase/plugin-china-region',
 | 
			
		||||
  '@nocobase/plugin-export',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
for (const plugin of plugins) {
 | 
			
		||||
 | 
			
		||||
@ -14,12 +14,12 @@ interface ActionParams {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Resource {
 | 
			
		||||
  get: (params?: ActionParams) => Promise<any>;
 | 
			
		||||
  list: (params?: ActionParams) => Promise<any>;
 | 
			
		||||
  create: (params?: ActionParams) => Promise<any>;
 | 
			
		||||
  update: (params?: ActionParams) => Promise<any>;
 | 
			
		||||
  destroy: (params?: ActionParams) => Promise<any>;
 | 
			
		||||
  [name: string]: (params?: ActionParams) => Promise<any>;
 | 
			
		||||
  get: (params?: ActionParams, options?: any) => Promise<any>;
 | 
			
		||||
  list: (params?: ActionParams, options?: any) => Promise<any>;
 | 
			
		||||
  create: (params?: ActionParams, options?: any) => Promise<any>;
 | 
			
		||||
  update: (params?: ActionParams, options?: any) => Promise<any>;
 | 
			
		||||
  destroy: (params?: ActionParams, options?: any) => Promise<any>;
 | 
			
		||||
  [name: string]: (params?: ActionParams, options?: any) => Promise<any>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO 待改进,提供一个封装度更完整的 SDK,request 可以自由替换
 | 
			
		||||
@ -29,7 +29,7 @@ class ApiClient {
 | 
			
		||||
      {},
 | 
			
		||||
      {
 | 
			
		||||
        get(target, method, receiver) {
 | 
			
		||||
          return (params: ActionParams = {}) => {
 | 
			
		||||
          return (params: ActionParams = {}, options: any = {}) => {
 | 
			
		||||
            let {
 | 
			
		||||
              associatedKey,
 | 
			
		||||
              resourceKey,
 | 
			
		||||
@ -41,15 +41,11 @@ class ApiClient {
 | 
			
		||||
            } = params;
 | 
			
		||||
            let url = `/${name}`;
 | 
			
		||||
            sort = sort || [];
 | 
			
		||||
            let options: any = {
 | 
			
		||||
              params: {},
 | 
			
		||||
            };
 | 
			
		||||
            if (['list', 'get'].indexOf(method as string) !== -1) {
 | 
			
		||||
            options.params = restParams;
 | 
			
		||||
            if (['list', 'get', 'export'].indexOf(method as string) !== -1) {
 | 
			
		||||
              options.method = 'get';
 | 
			
		||||
              options.params = restParams;
 | 
			
		||||
            } else {
 | 
			
		||||
              options.method = 'post';
 | 
			
		||||
              options.params = restParams;
 | 
			
		||||
              options.data = values;
 | 
			
		||||
            }
 | 
			
		||||
            if (associatedKey) {
 | 
			
		||||
 | 
			
		||||
@ -258,6 +258,52 @@ export function Table(props: any) {
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const exportRequest = useRequest(({ sorter, filter } = {}) => {
 | 
			
		||||
    return api
 | 
			
		||||
      .resource(resourceName)
 | 
			
		||||
      .export({
 | 
			
		||||
        associatedKey,
 | 
			
		||||
        perPage: -1,
 | 
			
		||||
        sorter,
 | 
			
		||||
        sort,
 | 
			
		||||
        'fields[appends]': appends,
 | 
			
		||||
        filter: {
 | 
			
		||||
          and: [
 | 
			
		||||
            defaultFilter,
 | 
			
		||||
            schemaFilter,
 | 
			
		||||
            filter,
 | 
			
		||||
          ].filter(obj => obj && Object.keys(obj).length),
 | 
			
		||||
        },
 | 
			
		||||
      }, {
 | 
			
		||||
        parseResponse: false,
 | 
			
		||||
        responseType: 'blob'
 | 
			
		||||
      })
 | 
			
		||||
      .then(async response => {
 | 
			
		||||
        // decodeURI() for encoded filename in server side
 | 
			
		||||
        const filename = decodeURI(response.headers.get('Content-Disposition').replace('attachment; filename=', ''));
 | 
			
		||||
        // ReadableStream
 | 
			
		||||
        let res = new Response(response.body);
 | 
			
		||||
        let blob = await res.blob();
 | 
			
		||||
        let url = URL.createObjectURL(blob);
 | 
			
		||||
        let a = document.createElement('a');
 | 
			
		||||
        a.style.display = 'none';
 | 
			
		||||
        a.href = url;
 | 
			
		||||
        a.download = filename;
 | 
			
		||||
        document.body.appendChild(a);
 | 
			
		||||
        a.click();
 | 
			
		||||
        // cleanup
 | 
			
		||||
        URL.revokeObjectURL(url);
 | 
			
		||||
        document.body.removeChild(a);
 | 
			
		||||
        a = null;
 | 
			
		||||
        blob = null;
 | 
			
		||||
        url = null;
 | 
			
		||||
        res = null;
 | 
			
		||||
      });
 | 
			
		||||
  }, {
 | 
			
		||||
    manual: true,
 | 
			
		||||
    paginated: false
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const currentRow = find(
 | 
			
		||||
    data && data.list,
 | 
			
		||||
    item => item[rowKey] == currentRowId,
 | 
			
		||||
@ -433,6 +479,14 @@ export function Table(props: any) {
 | 
			
		||||
              refresh();
 | 
			
		||||
              await reloadMenu();
 | 
			
		||||
            },
 | 
			
		||||
            async export() {
 | 
			
		||||
              exportRequest.run({
 | 
			
		||||
                ...params[0], 
 | 
			
		||||
                filter: selectedRowKeys.length ? {
 | 
			
		||||
                  [`${rowKey}.in`]: selectedRowKeys,
 | 
			
		||||
                } : {},
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          actions={actions}
 | 
			
		||||
          style={{ marginBottom: 14 }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								packages/plugin-export/.npmignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								packages/plugin-export/.npmignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
node_modules
 | 
			
		||||
*.log
 | 
			
		||||
docs
 | 
			
		||||
__tests__
 | 
			
		||||
tsconfig.json
 | 
			
		||||
src
 | 
			
		||||
.fatherrc.ts
 | 
			
		||||
							
								
								
									
										19
									
								
								packages/plugin-export/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/plugin-export/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "@nocobase/plugin-export",
 | 
			
		||||
  "version": "0.4.0-alpha.7",
 | 
			
		||||
  "main": "lib/index.js",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/node-xlsx": "^0.15.1"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@nocobase/actions": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/database": "^0.4.0-alpha.7",
 | 
			
		||||
    "@nocobase/resourcer": "^0.4.0-alpha.7",
 | 
			
		||||
    "node-xlsx": "^0.16.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
    "@nocobase/plugin-collections": "*",
 | 
			
		||||
    "@nocobase/plugin-permissions": "*"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								packages/plugin-export/src/actions/export.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								packages/plugin-export/src/actions/export.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
			
		||||
import xlsx from 'node-xlsx';
 | 
			
		||||
import { actions } from '@nocobase/actions';
 | 
			
		||||
import render from '../renders';
 | 
			
		||||
 | 
			
		||||
async function _export(ctx, next) {
 | 
			
		||||
  await actions.common.list(ctx, async () => {
 | 
			
		||||
    const {
 | 
			
		||||
      db,
 | 
			
		||||
      action: {
 | 
			
		||||
        params: {
 | 
			
		||||
          resourceName
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      body,
 | 
			
		||||
    } = ctx;
 | 
			
		||||
 | 
			
		||||
    const table = db.getTable(resourceName);
 | 
			
		||||
    const tableOptions = table.getOptions();
 | 
			
		||||
 | 
			
		||||
    const fields = body.rows.length
 | 
			
		||||
      ? Object.keys(body.rows[0].get())
 | 
			
		||||
        .map(key => tableOptions.fields.find(({ name }) => name === key))
 | 
			
		||||
        .filter(item => item && !item.developerMode)
 | 
			
		||||
        .sort((a, b) => a.sort - b.sort)
 | 
			
		||||
      : [];
 | 
			
		||||
 | 
			
		||||
    const { rows, ranges } = render({
 | 
			
		||||
      fields,
 | 
			
		||||
      data: body.rows
 | 
			
		||||
    }, ctx);
 | 
			
		||||
 | 
			
		||||
    ctx.body = xlsx.build([{
 | 
			
		||||
      name: tableOptions.title,
 | 
			
		||||
      data: rows,
 | 
			
		||||
      options: {
 | 
			
		||||
        '!merges': ranges
 | 
			
		||||
      }
 | 
			
		||||
    }]);
 | 
			
		||||
 | 
			
		||||
    ctx.set({
 | 
			
		||||
      'Content-Type': 'application/octet-stream',
 | 
			
		||||
      // to avoid "invalid character" error in header (RFC)
 | 
			
		||||
      'Content-Disposition': `attachment; filename=${encodeURI(tableOptions.title)}.xlsx`
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  await next();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default _export;
 | 
			
		||||
							
								
								
									
										162
									
								
								packages/plugin-export/src/renders/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								packages/plugin-export/src/renders/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,162 @@
 | 
			
		||||
import * as renders from './renders';
 | 
			
		||||
 | 
			
		||||
function getInterfaceRender(name: string): Function {
 | 
			
		||||
  return renders[name] || renders._;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderHeader(params, ctx) {
 | 
			
		||||
  const {
 | 
			
		||||
    fields,
 | 
			
		||||
    headers = [],
 | 
			
		||||
    rowIndex = 0
 | 
			
		||||
  } = params;
 | 
			
		||||
 | 
			
		||||
  let { colIndex = 0 } = params;
 | 
			
		||||
 | 
			
		||||
  if (!headers[rowIndex]) {
 | 
			
		||||
    headers.push([]);
 | 
			
		||||
  }
 | 
			
		||||
  const row = headers[rowIndex];
 | 
			
		||||
 | 
			
		||||
  fields.forEach((field, i) => {
 | 
			
		||||
    const nextColIndex = colIndex + i;
 | 
			
		||||
    row.push({
 | 
			
		||||
      field,
 | 
			
		||||
      rowIndex,
 | 
			
		||||
      colIndex: nextColIndex
 | 
			
		||||
    });
 | 
			
		||||
    if (field.interface === 'subTable') {
 | 
			
		||||
      const subTable = ctx.db.getTable(field.target);
 | 
			
		||||
      const subFields = subTable.getOptions().fields.filter(field => Boolean(field.__index));
 | 
			
		||||
      renderHeader({
 | 
			
		||||
        fields: subFields,
 | 
			
		||||
        headers,
 | 
			
		||||
        rowIndex: rowIndex + 1,
 | 
			
		||||
        colIndex: nextColIndex
 | 
			
		||||
      }, ctx);
 | 
			
		||||
      colIndex += subFields.length;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  Object.assign(params, { headers });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderRows({ fields, data }, ctx) {
 | 
			
		||||
  return data.reduce((result, row) => {
 | 
			
		||||
    const thisRow = [];
 | 
			
		||||
    const rowIndex = 0;
 | 
			
		||||
    let colOffset = 0;
 | 
			
		||||
    fields.forEach((field, i) => {
 | 
			
		||||
      if (!thisRow[rowIndex]) {
 | 
			
		||||
        thisRow.push([]);
 | 
			
		||||
      }
 | 
			
		||||
      const cells = thisRow[rowIndex];
 | 
			
		||||
      if (field.interface !== 'subTable') {
 | 
			
		||||
        const render = getInterfaceRender(field.interface);
 | 
			
		||||
        cells.push({
 | 
			
		||||
          value: render(field, row),
 | 
			
		||||
          rowIndex: result.length + rowIndex,
 | 
			
		||||
          colIndex: i + colOffset
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const subTable = ctx.db.getTable(field.target);
 | 
			
		||||
      const subFields = subTable.getOptions().fields.filter(item => Boolean(item.__index));
 | 
			
		||||
      const subRows = renderRows({ fields: subFields, data: row.get(field.name) || [] }, ctx);
 | 
			
		||||
 | 
			
		||||
      // const { rows: subRowGroups } = subTableRows;
 | 
			
		||||
      subRows.forEach((cells, j) => {
 | 
			
		||||
        const subRowIndex = rowIndex + j;
 | 
			
		||||
        if (!thisRow[subRowIndex]) {
 | 
			
		||||
          thisRow.push([]);
 | 
			
		||||
        }
 | 
			
		||||
        const subCells = thisRow[subRowIndex];
 | 
			
		||||
        subCells.push(...cells.map(cell => ({
 | 
			
		||||
          ...cell,
 | 
			
		||||
          rowIndex: result.length + subRowIndex,
 | 
			
		||||
          colIndex: cell.colIndex + i
 | 
			
		||||
        })));
 | 
			
		||||
      });
 | 
			
		||||
      colOffset += subFields.length;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    thisRow.forEach((cells) => {
 | 
			
		||||
      cells.forEach(cell => {
 | 
			
		||||
        const relRowIndex = cell.rowIndex - result.length;
 | 
			
		||||
        Object.assign(cell, {
 | 
			
		||||
          rowSpan: relRowIndex >= thisRow.length - 1
 | 
			
		||||
            || thisRow[relRowIndex + 1].find(item => item.colIndex === cell.colIndex)
 | 
			
		||||
            ? 1
 | 
			
		||||
            : thisRow.length - relRowIndex
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return result.concat(thisRow);
 | 
			
		||||
  }, []);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function({ fields, data }, ctx) {
 | 
			
		||||
  const headers = [];
 | 
			
		||||
  renderHeader({
 | 
			
		||||
    fields,
 | 
			
		||||
    headers
 | 
			
		||||
  }, ctx);
 | 
			
		||||
  const ranges = [];
 | 
			
		||||
  // 计算全表最大的列索引(由于无论如何最大列都是单个单元格,所以等价于长度)
 | 
			
		||||
  const maxColIndex = Math.max(...headers.map(row => row[row.length - 1].colIndex));
 | 
			
		||||
  // 遍历所有单元格,计算需要合并的坐标范围
 | 
			
		||||
  headers.forEach((row, rowIndex) => {
 | 
			
		||||
    row.forEach((cell, cellIndex) => {
 | 
			
		||||
      // 跨行合并的行数为
 | 
			
		||||
      cell.rowSpan = cell.rowIndex >= headers.length - 1
 | 
			
		||||
        || headers[cell.rowIndex + 1].find(item => item.colIndex === cell.colIndex)
 | 
			
		||||
        ? 1
 | 
			
		||||
        : headers.length - cell.rowIndex;
 | 
			
		||||
 | 
			
		||||
      const nextCell = headers.slice(0, rowIndex + 1)
 | 
			
		||||
        .map(r => r.find(item => item.colIndex > cell.colIndex))
 | 
			
		||||
        .filter(c => Boolean(c))
 | 
			
		||||
        .reduce((min, c) => min && Math.min(min.colIndex, c.colIndex) === min.colIndex ? min : c, null);
 | 
			
		||||
      cell.colSpan = nextCell
 | 
			
		||||
        ? nextCell.colIndex - cell.colIndex
 | 
			
		||||
        : maxColIndex - cell.colIndex + 1;
 | 
			
		||||
      
 | 
			
		||||
      if (cell.rowSpan > 1 || cell.colSpan > 1) {
 | 
			
		||||
        ranges.push({
 | 
			
		||||
          s: { c: cell.colIndex, r: cell.rowIndex },
 | 
			
		||||
          e: { c: cell.colIndex + cell.colSpan - 1, r: cell.rowIndex + cell.rowSpan - 1 }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const rows = renderRows({ fields, data }, ctx)
 | 
			
		||||
    .map(row => {
 | 
			
		||||
      const cells = Array(maxColIndex).fill(null);
 | 
			
		||||
      row.forEach(cell => {
 | 
			
		||||
        cells.splice(cell.colIndex, 1, cell.value);
 | 
			
		||||
        if (cell.rowSpan > 1) {
 | 
			
		||||
          ranges.push({
 | 
			
		||||
            s: { c: cell.colIndex, r: cell.rowIndex + headers.length },
 | 
			
		||||
            e: { c: cell.colIndex, r: cell.rowIndex + cell.rowSpan - 1 + headers.length }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      return cells;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    rows: [
 | 
			
		||||
      ...headers.map(row => {
 | 
			
		||||
        // 补齐无数据单元格,以供合并
 | 
			
		||||
        const cells = Array(maxColIndex).fill(null);
 | 
			
		||||
        row.forEach(cell => cells.splice(cell.colIndex, 1, cell.field.title))
 | 
			
		||||
        return cells;
 | 
			
		||||
      }),
 | 
			
		||||
      ...rows
 | 
			
		||||
    ],
 | 
			
		||||
    ranges
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								packages/plugin-export/src/renders/renders.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								packages/plugin-export/src/renders/renders.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
import moment from 'moment';
 | 
			
		||||
 | 
			
		||||
export function _(field, row) {
 | 
			
		||||
  return row.get(field.name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function datetime(field, row) {
 | 
			
		||||
  const value = row.get(field.name);
 | 
			
		||||
  return moment(value).format(field.showTime ? `${field.dateFormat} ${field.timeFormat}` : field.dateFormat);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function percent(field, row) {
 | 
			
		||||
  const value = row.get(field.name);
 | 
			
		||||
  return value && `${value}%`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function boolean(field, row) {
 | 
			
		||||
  const value = row.get(field.name);
 | 
			
		||||
  // TODO(feature): i18n
 | 
			
		||||
  return value ? '是' : '否';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function select(field, row) {
 | 
			
		||||
  const value = row.get(field.name);
 | 
			
		||||
  const option = field.dataSource.find(item => item.value === value);
 | 
			
		||||
  return option && option.label;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function multipleSelect(field, row) {
 | 
			
		||||
  const values = row.get(field.name);
 | 
			
		||||
  return values && values.map(value => {
 | 
			
		||||
    const option = field.dataSource.find(item => item.value === value);
 | 
			
		||||
    return option && option.label;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const radio = select;
 | 
			
		||||
 | 
			
		||||
export const checkboxes = multipleSelect;
 | 
			
		||||
 | 
			
		||||
export function subTable(field, row) {
 | 
			
		||||
  // TODO: need title field to be defined
 | 
			
		||||
  return (row.get(field.name) || []).map(item => item[field.sourceKey]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function linkTo(field, row) {
 | 
			
		||||
  return (row.get(field.name) || []).map(item => item[field.labelField]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function attachment(field, row) {
 | 
			
		||||
  return (row.get(field.name) || []).map(item => item[field.url]).join(' ');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function chinaRegion(field, row) {
 | 
			
		||||
  const value = row.get(field.name);
 | 
			
		||||
  const values = (Array.isArray(value) ? value : [value]).sort((a, b) =>
 | 
			
		||||
    a.level !== b.level ? a.level - b.level : a.sort - b.sort,
 | 
			
		||||
  );
 | 
			
		||||
  return values.map(item => item.name).join('/');
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								packages/plugin-export/src/server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								packages/plugin-export/src/server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import Resourcer from '@nocobase/resourcer';
 | 
			
		||||
import _export from './actions/export';
 | 
			
		||||
 | 
			
		||||
export const ACTION_NAME_EXPORT = 'export';
 | 
			
		||||
 | 
			
		||||
export default async function (options = {}) {
 | 
			
		||||
  const resourcer: Resourcer = this.resourcer;
 | 
			
		||||
 | 
			
		||||
  resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export);
 | 
			
		||||
 | 
			
		||||
  // TODO(temp): 继承 list 权限的临时写法
 | 
			
		||||
  resourcer.use(async (ctx, next) => {
 | 
			
		||||
    if (ctx.action.params.actionName === ACTION_NAME_EXPORT) {
 | 
			
		||||
      ctx.action.mergeParams({
 | 
			
		||||
        actionName: 'list'
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log('action name in export has been rewritten to:', ctx.action.params.actionName);
 | 
			
		||||
 | 
			
		||||
      const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions');
 | 
			
		||||
      if (permissionPlugin) {
 | 
			
		||||
        return permissionPlugin.middleware(ctx, next);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await next();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user