feat: Add AudioWidget (#7179)
* Create initial version of AudioWidget by copying VideoWidget * Add EventType for AUDIO * Change default Audio URL to a podcast related to Appsmith * Add AudioWidget icon * Change Entity definition for AudioWidget * Add cypress test * Add jest test * fix: typo
This commit is contained in:
parent
f3afa81afe
commit
8a45e1507e
49
app/client/cypress/fixtures/audioWidgetDsl.json
Normal file
49
app/client/cypress/fixtures/audioWidgetDsl.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"dsl": {
|
||||
"widgetName": "MainContainer",
|
||||
"backgroundColor": "none",
|
||||
"rightColumn": 915.6499999999999,
|
||||
"snapColumns": 64,
|
||||
"detachFromLayout": true,
|
||||
"widgetId": "0",
|
||||
"topRow": 0,
|
||||
"bottomRow": 750,
|
||||
"containerStyle": "none",
|
||||
"snapRows": 125,
|
||||
"parentRowSpace": 1,
|
||||
"type": "CANVAS_WIDGET",
|
||||
"canExtend": true,
|
||||
"version": 38,
|
||||
"minHeight": 760,
|
||||
"parentColumnSpace": 1,
|
||||
"dynamicBindingPathList": [],
|
||||
"leftColumn": 0,
|
||||
"children": [
|
||||
{
|
||||
"isVisible": true,
|
||||
"widgetName": "Audio1",
|
||||
"url": "https://cdn.simplecast.com/audio/10488ddf-3ca4-4300-9391-c2967d806334/episodes/8c8341f0-0a3a-4f2c-bfe0-0abb6b3c1c87/audio/03e2e3d8-e703-4953-adc0-e72687f31178/default_tc.mp3",
|
||||
"autoPlay": false,
|
||||
"version": 1,
|
||||
"type": "AUDIO_WIDGET",
|
||||
"hideCard": false,
|
||||
"displayName": "Audio",
|
||||
"key": "qdo3rra7pr",
|
||||
"iconSVG": "/static/media/icon.26d597b5.svg",
|
||||
"widgetId": "ttnnyvbw2c",
|
||||
"renderMode": "CANVAS",
|
||||
"isLoading": false,
|
||||
"parentColumnSpace": 14.609375,
|
||||
"parentRowSpace": 10,
|
||||
"leftColumn": 5,
|
||||
"rightColumn": 33,
|
||||
"topRow": 11,
|
||||
"bottomRow": 15,
|
||||
"parentId": "0",
|
||||
"dynamicBindingPathList": [],
|
||||
"dynamicTriggerPathList": []
|
||||
}
|
||||
],
|
||||
"dynamicTriggerPathList": []
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@
|
|||
"input2": "(//div[@class='bp3-input-group']//input)[1]",
|
||||
"input3": "(//div[@class='bp3-input-group']//input)[2]",
|
||||
"videoUrl": "https://www.youtube.com/watch?v=S5musXykVs0",
|
||||
"audioUrl": "https://cdn.simplecast.com/audio/10488ddf-3ca4-4300-9391-c2967d806334/episodes/8c8341f0-0a3a-4f2c-bfe0-0abb6b3c1c87/audio/03e2e3d8-e703-4953-adc0-e72687f31178/default_tc.mp3",
|
||||
"TablePagination": [
|
||||
{
|
||||
"id": 2381224,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
const widgetsPage = require("../../../../locators/Widgets.json");
|
||||
const commonlocators = require("../../../../locators/commonlocators.json");
|
||||
const dsl = require("../../../../fixtures/audioWidgetDsl.json");
|
||||
const testdata = require("../../../../fixtures/testdata.json");
|
||||
|
||||
describe("Audio Widget Functionality", function() {
|
||||
before(() => {
|
||||
cy.addDsl(dsl);
|
||||
});
|
||||
|
||||
it("Audio Widget play functionality validation", function() {
|
||||
cy.openPropertyPane("audiowidget");
|
||||
cy.widgetText("Audio1", widgetsPage.audioWidget, commonlocators.audioInner);
|
||||
cy.get(commonlocators.onPlay).click();
|
||||
cy.selectShowMsg();
|
||||
cy.addSuccessMessage("Play success");
|
||||
cy.get(widgetsPage.autoPlay).click();
|
||||
cy.wait("@updateLayout").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
it("Audio widget pause functionality validation", function() {
|
||||
cy.get(commonlocators.onPause).click();
|
||||
cy.selectShowMsg();
|
||||
cy.addSuccessMessage("Pause success");
|
||||
cy.get(widgetsPage.autoPlay).click();
|
||||
cy.wait("@updateLayout").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
it("Update audio url and check play and pause functionality validation", function() {
|
||||
cy.testCodeMirror(testdata.audioUrl);
|
||||
cy.get(".CodeMirror textarea")
|
||||
.first()
|
||||
.blur();
|
||||
cy.get(widgetsPage.autoPlay).click({ force: true });
|
||||
cy.wait("@updateLayout").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
cy.get(widgetsPage.autoPlay).click({ force: true });
|
||||
cy.wait("@updateLayout").should(
|
||||
"have.nested.property",
|
||||
"response.body.responseMeta.status",
|
||||
200,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -56,6 +56,7 @@
|
|||
"SearchTextChangeAction": ".t--property-control-onsearchtextchanged button",
|
||||
"tableSearchTextChangeSelected": ".t--property-control-onsearchtextchanged",
|
||||
"videoWidget": ".t--draggable-videowidget",
|
||||
"audioWidget": ".t--draggable-audiowidget",
|
||||
"autoPlay": ".t--property-control-autoplay > .bp3-control > .bp3-control-indicator",
|
||||
"defaultOption": ".t--property-control-defaultoption .CodeMirror-code",
|
||||
"dropdownSingleSelect": ".bp3-popover-target > div > .bp3-button",
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@
|
|||
"toastAction": ".t--toast-action",
|
||||
"toastBody": ".Toastify__toast-body",
|
||||
"videoInner": ".t--draggable-videowidget span.t--widget-name",
|
||||
"audioInner": ".t--draggable-audiowidget span.t--widget-name",
|
||||
"onPlay": ".t--property-control-onplay .t--open-dropdown-Select-Action",
|
||||
"chooseAction": ".single-select",
|
||||
"chooseMsgType": ".t--open-dropdown-Select-type",
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@ export enum EventType {
|
|||
ON_VIDEO_END = "ON_VIDEO_END",
|
||||
ON_VIDEO_PLAY = "ON_VIDEO_PLAY",
|
||||
ON_VIDEO_PAUSE = "ON_VIDEO_PAUSE",
|
||||
ON_AUDIO_START = "ON_AUDIO_START",
|
||||
ON_AUDIO_END = "ON_AUDIO_END",
|
||||
ON_AUDIO_PLAY = "ON_AUDIO_PLAY",
|
||||
ON_AUDIO_PAUSE = "ON_AUDIO_PAUSE",
|
||||
ON_RATE_CHANGED = "ON_RATE_CHANGED",
|
||||
ON_IFRAME_URL_CHANGED = "ON_IFRAME_URL_CHANGED",
|
||||
ON_IFRAME_MESSAGE_RECEIVED = "ON_IFRAME_MESSAGE_RECEIVED",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
export const HelpMap: Record<string, { path: string; searchKey: string }> = {
|
||||
AUDIO_WIDGET: {
|
||||
path: "/widget-reference/audio",
|
||||
searchKey: "Audio",
|
||||
},
|
||||
CONTAINER_WIDGET: {
|
||||
path: "/widget-reference/container",
|
||||
searchKey: "Container",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const FORM_VALIDATION_EMPTY_EMAIL = () => `Please enter an email`;
|
|||
export const FORM_VALIDATION_INVALID_EMAIL = () =>
|
||||
`Please provide a valid email address`;
|
||||
export const ENTER_VIDEO_URL = () => `Please provide a valid url`;
|
||||
export const ENTER_AUDIO_URL = () => `Please provide a valid url`;
|
||||
|
||||
export const FORM_VALIDATION_EMPTY_PASSWORD = () => `Please enter the password`;
|
||||
export const FORM_VALIDATION_PASSWORD_RULE = () =>
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ import StatboxWidget, {
|
|||
import FilePickerWidgetV2, {
|
||||
CONFIG as FILEPICKER_WIDGET_V2_CONFIG,
|
||||
} from "widgets/FilePickerWidgetV2";
|
||||
import AudioWidget, {
|
||||
CONFIG as AUDIO_WIDGET_CONFIG,
|
||||
} from "widgets/AudioWidget";
|
||||
|
||||
import AudioRecorderWidget, {
|
||||
CONFIG as AUDIO_RECORDER_WIDGET_CONFIG,
|
||||
|
|
@ -143,6 +146,7 @@ export const registerWidgets = () => {
|
|||
registerWidget(AudioRecorderWidget, AUDIO_RECORDER_WIDGET_CONFIG);
|
||||
registerWidget(MultiSelectTreeWidget, MULTI_SELECT_TREE_WIDGET_CONFIG);
|
||||
registerWidget(SingleSelectTreeWidget, SINGLE_SELECT_TREE_WIDGET_CONFIG);
|
||||
registerWidget(AudioWidget, AUDIO_WIDGET_CONFIG);
|
||||
|
||||
log.debug("Widget registration took: ", performance.now() - start, "ms");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ export const entityDefinitions: Record<string, unknown> = {
|
|||
clear: "fn() -> void",
|
||||
};
|
||||
},
|
||||
AUDIO_WIDGET: {
|
||||
"!doc":
|
||||
"Audio widget can be used for playing a variety of audio formats like MP3, AAC etc.",
|
||||
"!url": "https://docs.appsmith.com/widget-reference/audio",
|
||||
playState: "number",
|
||||
autoPlay: "bool",
|
||||
},
|
||||
CONTAINER_WIDGET: {
|
||||
"!doc":
|
||||
"Containers are used to group widgets together to form logical higher order widgets. Containers let you organize your page better and move all the widgets inside them together.",
|
||||
|
|
|
|||
68
app/client/src/widgets/AudioWidget/component/index.tsx
Normal file
68
app/client/src/widgets/AudioWidget/component/index.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import ReactPlayer from "react-player";
|
||||
import React, { Ref } from "react";
|
||||
import styled from "styled-components";
|
||||
import { createMessage, ENTER_AUDIO_URL } from "constants/messages";
|
||||
export interface AudioComponentProps {
|
||||
url?: string;
|
||||
autoplay?: boolean;
|
||||
controls?: boolean;
|
||||
onStart?: () => void;
|
||||
onPlay?: () => void;
|
||||
onPause?: () => void;
|
||||
onEnded?: () => void;
|
||||
onReady?: () => void;
|
||||
onProgress?: () => void;
|
||||
onSeek?: () => void;
|
||||
onError?: () => void;
|
||||
player?: Ref<ReactPlayer>;
|
||||
}
|
||||
|
||||
const ErrorContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Error = styled.span``;
|
||||
|
||||
export default function AudioComponent(props: AudioComponentProps) {
|
||||
const {
|
||||
autoplay,
|
||||
controls,
|
||||
onEnded,
|
||||
onError,
|
||||
onPause,
|
||||
onPlay,
|
||||
onProgress,
|
||||
onReady,
|
||||
onSeek,
|
||||
onStart,
|
||||
player,
|
||||
url,
|
||||
} = props;
|
||||
return url ? (
|
||||
<ReactPlayer
|
||||
controls={controls || true}
|
||||
height="100%"
|
||||
onEnded={onEnded}
|
||||
onError={onError}
|
||||
onPause={onPause}
|
||||
onPlay={onPlay}
|
||||
onProgress={onProgress}
|
||||
onReady={onReady}
|
||||
onSeek={onSeek}
|
||||
onStart={onStart}
|
||||
pip={false}
|
||||
playing={autoplay}
|
||||
ref={player}
|
||||
url={url}
|
||||
width="100%"
|
||||
/>
|
||||
) : (
|
||||
<ErrorContainer>
|
||||
<Error>{createMessage(ENTER_AUDIO_URL)}</Error>
|
||||
</ErrorContainer>
|
||||
);
|
||||
}
|
||||
8
app/client/src/widgets/AudioWidget/icon.svg
Normal file
8
app/client/src/widgets/AudioWidget/icon.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 11V3H2.5V11H0.5Z" fill="#EAEAEA"/>
|
||||
<path d="M3.5 13V1H5.5V13H3.5Z" fill="#EAEAEA"/>
|
||||
<path d="M6.5 10V5H8.5V10H6.5Z" fill="#EAEAEA"/>
|
||||
<path d="M9.5 14V0H11.5V14H9.5Z" fill="#EAEAEA"/>
|
||||
<path d="M12.5 12V2H14.5V12H12.5Z" fill="#EAEAEA"/>
|
||||
<path d="M15.5 10V4H17.5V10H15.5Z" fill="#EAEAEA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
27
app/client/src/widgets/AudioWidget/index.tsx
Normal file
27
app/client/src/widgets/AudioWidget/index.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import Widget from "./widget";
|
||||
import IconSVG from "./icon.svg";
|
||||
import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants";
|
||||
|
||||
export const CONFIG = {
|
||||
type: Widget.getWidgetType(),
|
||||
name: "Audio",
|
||||
iconSVG: IconSVG,
|
||||
needsMeta: true,
|
||||
defaults: {
|
||||
rows: 1 * GRID_DENSITY_MIGRATION_V1,
|
||||
columns: 7 * GRID_DENSITY_MIGRATION_V1,
|
||||
widgetName: "Audio",
|
||||
url:
|
||||
"https://cdn.simplecast.com/audio/10488ddf-3ca4-4300-9391-c2967d806334/episodes/8c8341f0-0a3a-4f2c-bfe0-0abb6b3c1c87/audio/03e2e3d8-e703-4953-adc0-e72687f31178/default_tc.mp3",
|
||||
autoPlay: false,
|
||||
version: 1,
|
||||
},
|
||||
properties: {
|
||||
derived: Widget.getDerivedPropertiesMap(),
|
||||
default: Widget.getDefaultPropertiesMap(),
|
||||
meta: Widget.getMetaPropertiesMap(),
|
||||
config: Widget.getPropertyPaneConfig(),
|
||||
},
|
||||
};
|
||||
|
||||
export default Widget;
|
||||
40
app/client/src/widgets/AudioWidget/widget/index.test.tsx
Normal file
40
app/client/src/widgets/AudioWidget/widget/index.test.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { PropertyPaneControlConfig } from "constants/PropertyControlConstants";
|
||||
import AudioWidget from ".";
|
||||
|
||||
const urlTests = [
|
||||
{ url: "https://appsmith.com/", isValid: true },
|
||||
{ url: "http://appsmith.com/", isValid: true },
|
||||
{ url: "appsmith.com/", isValid: true },
|
||||
{ url: "appsmith.com", isValid: true },
|
||||
{ url: "release.appsmith.com", isValid: true },
|
||||
{ url: "appsmith.com/audio.mp3", isValid: true },
|
||||
{ url: "appsmith./audio.mp3", isValid: false },
|
||||
{ url: "https://appsmith.com/randompath/somefile.mp3", isValid: true },
|
||||
{ url: "https://appsmith.com/randompath/some file.mp3", isValid: true },
|
||||
{ url: "random string", isValid: false },
|
||||
{
|
||||
url: "blob:https://dev.appsmith.com/9db94f56-5e32-4b18-2758-64c21a7f4610",
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
|
||||
describe("urlRegexValidation", () => {
|
||||
const generalSectionProperties: PropertyPaneControlConfig[] = AudioWidget.getPropertyPaneConfig().filter(
|
||||
(x) => x.sectionName === "General",
|
||||
)[0].children;
|
||||
const urlPropertyControl = generalSectionProperties.filter(
|
||||
(x) => x.propertyName === "url",
|
||||
)[0];
|
||||
const regEx = urlPropertyControl.validation?.params?.regex;
|
||||
|
||||
it("validate existence of regEx", () => {
|
||||
expect(regEx).toBeDefined();
|
||||
});
|
||||
|
||||
it("test regEx", () => {
|
||||
urlTests.forEach((test) => {
|
||||
if (test.isValid) expect(test.url).toMatch(regEx || "");
|
||||
else expect(test.url).not.toMatch(regEx || "");
|
||||
});
|
||||
});
|
||||
});
|
||||
175
app/client/src/widgets/AudioWidget/widget/index.tsx
Normal file
175
app/client/src/widgets/AudioWidget/widget/index.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import React, { Suspense, lazy } from "react";
|
||||
import BaseWidget, { WidgetProps, WidgetState } from "../../BaseWidget";
|
||||
import { WidgetType } from "constants/WidgetConstants";
|
||||
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
|
||||
import { ValidationTypes } from "constants/WidgetValidation";
|
||||
import Skeleton from "components/utils/Skeleton";
|
||||
import { retryPromise } from "utils/AppsmithUtils";
|
||||
import ReactPlayer from "react-player";
|
||||
import { AutocompleteDataType } from "utils/autocomplete/TernServer";
|
||||
|
||||
const AudioComponent = lazy(() => retryPromise(() => import("../component")));
|
||||
|
||||
export enum PlayState {
|
||||
NOT_STARTED = "NOT_STARTED",
|
||||
PAUSED = "PAUSED",
|
||||
ENDED = "ENDED",
|
||||
PLAYING = "PLAYING",
|
||||
}
|
||||
|
||||
class AudioWidget extends BaseWidget<AudioWidgetProps, WidgetState> {
|
||||
static getPropertyPaneConfig() {
|
||||
return [
|
||||
{
|
||||
sectionName: "General",
|
||||
children: [
|
||||
{
|
||||
propertyName: "url",
|
||||
label: "URL",
|
||||
controlType: "INPUT_TEXT",
|
||||
placeholderText: "Enter url",
|
||||
inputType: "TEXT",
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: {
|
||||
type: ValidationTypes.TEXT,
|
||||
params: {
|
||||
regex: /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/,
|
||||
expected: {
|
||||
type: "Audio URL",
|
||||
example:
|
||||
"https://cdn.simplecast.com/audio/10488ddf-3ca4-4300-9391-c2967d806334/episodes/8c8341f0-0a3a-4f2c-bfe0-0abb6b3c1c87/audio/03e2e3d8-e703-4953-adc0-e72687f31178/default_tc.mp3",
|
||||
autocompleteDataType: AutocompleteDataType.STRING,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
propertyName: "autoPlay",
|
||||
label: "Auto Play",
|
||||
helpText: "Audio will be automatically played",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
{
|
||||
helpText: "Controls the visibility of the widget",
|
||||
propertyName: "isVisible",
|
||||
label: "Visible",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: false,
|
||||
validation: { type: ValidationTypes.BOOLEAN },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionName: "Actions",
|
||||
children: [
|
||||
{
|
||||
helpText: "Triggers an action when the audio is played",
|
||||
propertyName: "onPlay",
|
||||
label: "onPlay",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
{
|
||||
helpText: "Triggers an action when the audio is paused",
|
||||
propertyName: "onPause",
|
||||
label: "onPause",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
{
|
||||
helpText: "Triggers an action when the audio ends",
|
||||
propertyName: "onEnd",
|
||||
label: "onEnd",
|
||||
controlType: "ACTION_SELECTOR",
|
||||
isJSConvertible: true,
|
||||
isBindProperty: true,
|
||||
isTriggerProperty: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private _player = React.createRef<ReactPlayer>();
|
||||
|
||||
static getMetaPropertiesMap(): Record<string, any> {
|
||||
return {
|
||||
playState: PlayState.NOT_STARTED,
|
||||
};
|
||||
}
|
||||
|
||||
static getDefaultPropertiesMap(): Record<string, string> {
|
||||
return {};
|
||||
}
|
||||
|
||||
getPageView() {
|
||||
const { autoPlay, onEnd, onPause, onPlay, url } = this.props;
|
||||
return (
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<AudioComponent
|
||||
autoplay={autoPlay}
|
||||
controls
|
||||
onEnded={() => {
|
||||
this.props.updateWidgetMetaProperty("playState", PlayState.ENDED, {
|
||||
triggerPropertyName: "onEnd",
|
||||
dynamicString: onEnd,
|
||||
event: {
|
||||
type: EventType.ON_AUDIO_END,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onPause={() => {
|
||||
//TODO: We do not want the pause event for onSeek or onEnd.
|
||||
this.props.updateWidgetMetaProperty("playState", PlayState.PAUSED, {
|
||||
triggerPropertyName: "onPause",
|
||||
dynamicString: onPause,
|
||||
event: {
|
||||
type: EventType.ON_AUDIO_PAUSE,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onPlay={() => {
|
||||
this.props.updateWidgetMetaProperty(
|
||||
"playState",
|
||||
PlayState.PLAYING,
|
||||
{
|
||||
triggerPropertyName: "onPlay",
|
||||
dynamicString: onPlay,
|
||||
event: {
|
||||
type: EventType.ON_AUDIO_PLAY,
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
player={this._player}
|
||||
url={url}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
static getWidgetType(): WidgetType {
|
||||
return "AUDIO_WIDGET";
|
||||
}
|
||||
}
|
||||
|
||||
export interface AudioWidgetProps extends WidgetProps {
|
||||
url: string;
|
||||
autoPlay: boolean;
|
||||
onPause?: string;
|
||||
onPlay?: string;
|
||||
onEnd?: string;
|
||||
}
|
||||
|
||||
export default AudioWidget;
|
||||
Loading…
Reference in New Issue
Block a user