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:
Aswath K 2021-09-24 21:35:53 +05:30 committed by GitHub
parent f3afa81afe
commit 8a45e1507e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 445 additions and 0 deletions

View 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": []
}
}

View File

@ -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,

View File

@ -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,
);
});
});

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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 = () =>

View File

@ -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");
};

View File

@ -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.",

View 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>
);
}

View 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

View 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;

View 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 || "");
});
});
});

View 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;