feat: add iframe-block plugin (#1281)

* feat: iframe-block plugin done

* refactor: iframe html filed type changed

* refactor: remove built-in actions in the ACL

* refactor: use built-in resource action

* fix: add iframe-block in built-in plugins

* refactor: remove id collection schema

* fix: fix iframe-block permission

* fix: fix iframe-block permission

* fix: improve code

* fix: src

* fix: bug

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
SemmyWong 2022-12-24 16:29:20 +08:00 committed by GitHub
parent 98bad0c8ea
commit 6d1a4425f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 348 additions and 89 deletions

View File

@ -0,0 +1 @@
export { default } from '@nocobase/plugin-iframe-block/client';

View File

@ -1,66 +0,0 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useDesignable } from '../../hooks';
export const IframeDesigner = () => {
const field = useField();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
return (
<GeneralSchemaDesigner>
<SchemaSettings.ModalItem
title={t('Edit iframe')}
schema={
{
type: 'object',
title: t('Edit chart'),
properties: {
url: {
title: t('URL'),
type: 'string',
default: fieldSchema?.['x-component-props']?.['url'],
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
height: {
title: t('Height'),
type: 'string',
default: fieldSchema?.['x-component-props']?.height || '60vh',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
},
} as ISchema
}
// {{ fetchData(api, { url: 'chartData:get' }) }}
onSubmit={async ({ url, height }) => {
field.componentProps.url = url;
field.componentProps.height = height;
const componentProps = fieldSchema['x-component-props'] || {};
componentProps['url'] = url;
componentProps['height'] = height;
fieldSchema['x-component-props'] = componentProps;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-component-props': componentProps,
},
});
}}
/>
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -1,4 +1,2 @@
export * from './dnd-context'; export * from './dnd-context';
export * from './iframe';
export * from './sortable-item'; export * from './sortable-item';

View File

@ -164,12 +164,6 @@ export const BlockInitializers = {
title: '{{t("Markdown")}}', title: '{{t("Markdown")}}',
component: 'MarkdownBlockInitializer', component: 'MarkdownBlockInitializer',
}, },
{
key: 'iframe',
type: 'item',
title: '{{t("Iframe")}}',
component: 'IframeBlockInitializer',
},
{ {
key: 'auditLogs', key: 'auditLogs',
type: 'item', type: 'item',

View File

@ -17,7 +17,6 @@ export * from './DetailsBlockInitializer';
export * from './FilterActionInitializer'; export * from './FilterActionInitializer';
export * from './FormBlockInitializer'; export * from './FormBlockInitializer';
export * from './G2PlotInitializer'; export * from './G2PlotInitializer';
export * from './IframeBlockInitializer';
export * from './InitializerWithSwitch'; export * from './InitializerWithSwitch';
export * from './KanbanBlockInitializer'; export * from './KanbanBlockInitializer';
export * from './MarkdownBlockInitializer'; export * from './MarkdownBlockInitializer';
@ -38,4 +37,3 @@ export * from './TableSelectorInitializer';
export * from './UpdateActionInitializer'; export * from './UpdateActionInitializer';
export * from './UpdateSubmitActionInitializer'; export * from './UpdateSubmitActionInitializer';
export * from './ViewActionInitializer'; export * from './ViewActionInitializer';

View File

@ -22,7 +22,7 @@ import {
useAPIClient, useAPIClient,
useCollection, useCollection,
useCompile, useCompile,
useDesignable, useDesignable
} from '..'; } from '..';
import { useSchemaTemplateManager } from '../schema-templates'; import { useSchemaTemplateManager } from '../schema-templates';
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate'; import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
@ -574,7 +574,18 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => {
}); });
SchemaSettings.ModalItem = (props) => { SchemaSettings.ModalItem = (props) => {
const { hidden, title, components, scope, effects, schema, onSubmit, initialValues, ...others } = props; const {
hidden,
title,
components,
scope,
effects,
schema,
onSubmit,
asyncGetInitialValues,
initialValues,
...others
} = props;
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
const cm = useContext(CollectionManagerContext); const cm = useContext(CollectionManagerContext);
if (hidden) { if (hidden) {
@ -583,7 +594,8 @@ SchemaSettings.ModalItem = (props) => {
return ( return (
<SchemaSettings.Item <SchemaSettings.Item
{...others} {...others}
onClick={() => { onClick={async () => {
const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues;
FormDialog(schema.title || title, () => { FormDialog(schema.title || title, () => {
return ( return (
<CollectionManagerContext.Provider value={cm}> <CollectionManagerContext.Provider value={cm}>
@ -596,7 +608,7 @@ SchemaSettings.ModalItem = (props) => {
); );
}) })
.open({ .open({
initialValues, initialValues: values,
effects, effects,
}) })
.then((values) => { .then((values) => {

View File

@ -56,7 +56,8 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
ctx.getBearerToken = () => { ctx.getBearerToken = () => {
return ctx.get('Authorization').replace(/^Bearer\s+/gi, ''); const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
return token || ctx.query.token;
}; };
await next(); await next();
}); });

View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/client';
export { default } from './lib/client';

View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/client"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,9 @@
{
"name": "@nocobase/plugin-iframe-block",
"version": "0.8.0-alpha.13",
"main": "lib/server/index.js",
"devDependencies": {
"@nocobase/server": "0.8.0-alpha.13",
"@nocobase/test": "0.8.0-alpha.13"
}
}

View File

@ -0,0 +1,4 @@
// @ts-nocheck
export * from './lib/server';
export { default } from './lib/server';

View File

@ -0,0 +1,30 @@
"use strict";
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _index = _interopRequireWildcard(require("./lib/server"));
Object.defineProperty(exports, "__esModule", {
value: true
});
var _exportNames = {};
Object.defineProperty(exports, "default", {
enumerable: true,
get: function get() {
return _index.default;
}
});
Object.keys(_index).forEach(function (key) {
if (key === "default" || key === "__esModule") return;
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
if (key in exports && exports[key] === _index[key]) return;
Object.defineProperty(exports, key, {
enumerable: true,
get: function get() {
return _index[key];
}
});
});

View File

@ -0,0 +1,131 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { GeneralSchemaDesigner, SchemaSettings, useAPIClient, useDesignable } from '@nocobase/client';
import React from 'react';
import { useTranslation } from 'react-i18next';
export const IframeDesigner = () => {
const field = useField();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
const { dn } = useDesignable();
const api = useAPIClient();
const { mode, url, htmlId, height = '60vh' } = fieldSchema['x-component-props'] || {};
const saveHtml = async (html: string) => {
const options = {
values: { html },
};
if (htmlId) {
const { data } = await api.resource('iframeHtml').update?.({ ...options, filterByTk: htmlId });
return data?.data?.[0] || { id: htmlId };
} else {
const { data } = await api.resource('iframeHtml').create?.(options);
return data?.data;
}
};
const submitHandler = async ({ mode, url, html, height }) => {
const componentProps = fieldSchema['x-component-props'] || {};
componentProps['mode'] = mode;
componentProps['height'] = height;
componentProps['url'] = url;
if (mode === 'html') {
const data = await saveHtml(html);
componentProps['htmlId'] = data.id;
}
fieldSchema['x-component-props'] = componentProps;
field.componentProps = { ...componentProps };
field.data = { v: uid() };
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
'x-component-props': componentProps,
},
});
};
return (
<GeneralSchemaDesigner>
<SchemaSettings.ModalItem
title={t('Edit iframe')}
asyncGetInitialValues={async () => {
const values = {
mode,
url,
height,
};
if (htmlId) {
const { data } = await api.resource('iframeHtml').get?.({ filterByTk: htmlId });
values['html'] = data?.data?.html || '';
}
return values;
}}
schema={
{
type: 'object',
title: t('Edit iframe'),
properties: {
mode: {
title: '{{t("Mode")}}',
'x-component': 'Radio.Group',
'x-decorator': 'FormItem',
required: true,
default: 'url',
enum: [
{ value: 'url', label: t('URL') },
{ value: 'html', label: t('html') },
],
},
url: {
title: t('URL'),
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
'x-reactions': {
dependencies: ['mode'],
fulfill: {
state: {
hidden: '{{$deps[0] === "html"}}',
},
},
},
},
html: {
title: t('html'),
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
required: true,
'x-reactions': {
dependencies: ['mode'],
fulfill: {
state: {
hidden: '{{$deps[0] === "url"}}',
},
},
},
},
height: {
title: t('Height'),
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
},
} as ISchema
}
onSubmit={submitHandler}
/>
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -1,4 +1,5 @@
import { observer } from '@formily/react'; import { observer, useField } from '@formily/react';
import { useAPIClient } from '@nocobase/client';
import { Card } from 'antd'; import { Card } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -6,7 +7,7 @@ import RIframe from 'react-iframe';
import type { IIframe } from 'react-iframe/types'; import type { IIframe } from 'react-iframe/types';
import { IframeDesigner } from './Iframe.Designer'; import { IframeDesigner } from './Iframe.Designer';
function isNumeric(str: string) { function isNumeric(str: string | undefined) {
if (typeof str !== 'string') return false; // we only process strings! if (typeof str !== 'string') return false; // we only process strings!
return ( return (
!isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)... !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
@ -14,15 +15,27 @@ function isNumeric(str: string) {
); // ...and ensure strings of whitespace fail ); // ...and ensure strings of whitespace fail
} }
export const Iframe: any = observer((props: IIframe) => { export const Iframe: any = observer((props: IIframe & { html?: string; htmlId?: number; mode: string }) => {
const { url, height, ...others } = props; const { url, htmlId, mode = 'url', height, html, ...others } = props;
const { t } = useTranslation(); const { t } = useTranslation();
if (!url) { const api = useAPIClient();
const field = useField();
const src = React.useMemo(() => {
if (mode === 'html') {
return `/api/iframeHtml:getHtml/${htmlId}?token=${api.auth.getToken()}&v=${field.data?.v || ''}`;
}
return url;
}, [url, mode, htmlId, field.data?.v]);
if ((mode === 'url' && !url) || (mode === 'html' && !htmlId)) {
return <Card style={{ marginBottom: 24 }}>{t('Please fill in the iframe URL')}</Card>; return <Card style={{ marginBottom: 24 }}>{t('Please fill in the iframe URL')}</Card>;
} }
return ( return (
<RIframe <RIframe
url={url} url={src}
width="100%" width="100%"
display="block" display="block"
position="relative" position="relative"

View File

@ -1,9 +1,8 @@
import { FormOutlined } from '@ant-design/icons'; import { FormOutlined } from '@ant-design/icons';
import { SchemaInitializer } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer';
export const IframeBlockInitializer = (props) => { export const IframeBlockInitializer = (props) => {
const { insert } = props; const { insert } = props;
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -0,0 +1,18 @@
import { SchemaInitializerContext } from '@nocobase/client';
import { useContext } from 'react';
export const IframeBlockInitializerProvider = (props: any) => {
const initializes = useContext(SchemaInitializerContext);
const mediaItem = initializes.BlockInitializers.items.find((item) => item.key === 'media');
const hasIframeBlockInitializer = mediaItem.children.some(
(initialize) => initialize.component === 'IframeBlockInitializer',
);
!hasIframeBlockInitializer &&
mediaItem.children.push({
key: 'iframe',
type: 'item',
title: '{{t("Iframe")}}',
component: 'IframeBlockInitializer',
});
return props.children;
};

View File

@ -0,0 +1,13 @@
import { SchemaComponentOptions } from '@nocobase/client';
import React from 'react';
import { Iframe } from './Iframe';
import { IframeBlockInitializer } from './IframeBlockInitializer';
import { IframeBlockInitializerProvider } from './IframeBlockInitializerProvider';
export const IframeBlockPlugin = (props: any) => {
return (
<SchemaComponentOptions components={{ Iframe, IframeBlockInitializer }}>
<IframeBlockInitializerProvider>{props.children}</IframeBlockInitializerProvider>
</SchemaComponentOptions>
);
};

View File

@ -0,0 +1 @@
export { IframeBlockPlugin as default } from './IframeBlockPlugin';

View File

@ -0,0 +1 @@
export { default } from './server';

View File

@ -0,0 +1,17 @@
import { Context, Next } from '@nocobase/actions';
import { Repository } from '@nocobase/database';
export async function getHtml(ctx: Context, next: Next) {
let { filterByTk } = ctx.action.params;
const { resourceName } = ctx.action;
const repository = ctx.db.getRepository<any>(resourceName) as Repository;
const model = await repository.findById(filterByTk);
ctx.body = model.get('html');
ctx.withoutDataWrapping = true;
ctx.set({
'Content-Type': 'text/html; charset=UTF-8',
});
await next();
}

View File

@ -0,0 +1,18 @@
import { CollectionOptions } from '@nocobase/database';
export default {
name: 'iframeHtml',
createdBy: true,
updatedBy: true,
fields: [
{
type: 'uid',
name: 'id',
primaryKey: true,
},
{
type: 'text',
name: 'html',
},
],
} as CollectionOptions;

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,30 @@
import { InstallOptions, Plugin } from '@nocobase/server';
import path from 'path';
import { getHtml } from './actions';
export class IframeBlockPlugin extends Plugin {
afterAdd() {}
beforeLoad() {}
async load() {
await this.db.import({
directory: path.resolve(__dirname, 'collections'),
});
this.app.acl.allow('iframeHtml', ['get', 'create', 'update', 'destroy'], 'allowConfigure');
this.app.acl.allow('iframeHtml', 'getHtml', 'loggedIn');
this.app.actions({
'iframeHtml:getHtml': getHtml,
});
}
async install(options?: InstallOptions) {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default IframeBlockPlugin;

View File

@ -31,6 +31,7 @@
"@nocobase/plugin-verification": "0.8.0-alpha.13", "@nocobase/plugin-verification": "0.8.0-alpha.13",
"@nocobase/plugin-workflow": "0.8.0-alpha.13", "@nocobase/plugin-workflow": "0.8.0-alpha.13",
"@nocobase/plugin-map": "0.8.0-alpha.13", "@nocobase/plugin-map": "0.8.0-alpha.13",
"@nocobase/plugin-iframe-block": "0.8.0-alpha.13",
"@nocobase/server": "0.8.0-alpha.13" "@nocobase/server": "0.8.0-alpha.13"
}, },
"repository": { "repository": {

View File

@ -23,6 +23,7 @@ export class PresetNocoBase extends Plugin {
'export', 'export',
'import', 'import',
'audit-logs', 'audit-logs',
'iframe-block',
].concat(plugins), ].concat(plugins),
); );
} }