From 15f84203c768b895fa6b50a44225eaa6d2b1ab2c Mon Sep 17 00:00:00 2001
From: satbir121 <39981226+satbir121@users.noreply.github.com>
Date: Sat, 26 Sep 2020 18:29:33 +0530
Subject: [PATCH] Video widget (#684)
* Working version of video player.
* Adding default video url
* Reducing the default widget of video widget
* Fixed tests
* Adding video icon.
* Removing commented code.
* Adding playState, onPause, onStart, onEnd.
* Adding onPlay event.
* Fixing onPlay
* Adding isVisible field.
* Changing video icon.
* Fixing Popover table video.
* Adding an error message for no url.
---
app/client/src/assets/icons/widget/video.svg | 3 +
.../designSystems/appsmith/PopoverVideo.tsx | 64 ++++++++
.../designSystems/appsmith/TableUtilities.tsx | 4 +-
.../designSystems/appsmith/VideoComponent.tsx | 137 +++++++++---------
app/client/src/constants/ActionConstants.tsx | 4 +
.../src/constants/FieldExpectedValue.ts | 5 +
app/client/src/constants/HelpConstants.ts | 4 +
app/client/src/constants/WidgetConstants.tsx | 1 +
app/client/src/constants/messages.ts | 2 +
app/client/src/icons/WidgetIcons.tsx | 6 +
.../PropertyPaneConfigResponse.tsx | 70 +++++++++
.../mockResponses/WidgetConfigResponse.tsx | 8 +
.../mockResponses/WidgetSidebarResponse.tsx | 5 +
.../entityReducers/widgetConfigReducer.tsx | 2 +
app/client/src/utils/WidgetRegistry.tsx | 19 +++
.../utils/autocomplete/EntityDefinitions.ts | 7 +
app/client/src/widgets/VideoWidget.tsx | 135 +++++++++++++++++
17 files changed, 405 insertions(+), 71 deletions(-)
create mode 100644 app/client/src/assets/icons/widget/video.svg
create mode 100644 app/client/src/components/designSystems/appsmith/PopoverVideo.tsx
create mode 100644 app/client/src/widgets/VideoWidget.tsx
diff --git a/app/client/src/assets/icons/widget/video.svg b/app/client/src/assets/icons/widget/video.svg
new file mode 100644
index 0000000000..11426cec91
--- /dev/null
+++ b/app/client/src/assets/icons/widget/video.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/client/src/components/designSystems/appsmith/PopoverVideo.tsx b/app/client/src/components/designSystems/appsmith/PopoverVideo.tsx
new file mode 100644
index 0000000000..57648add7c
--- /dev/null
+++ b/app/client/src/components/designSystems/appsmith/PopoverVideo.tsx
@@ -0,0 +1,64 @@
+import React from "react";
+import {
+ Popover,
+ PopoverInteractionKind,
+ PopoverPosition,
+} from "@blueprintjs/core";
+import { Colors } from "constants/Colors";
+import VideoComponent, { VideoComponentProps } from "./VideoComponent";
+import styled, { AnyStyledComponent } from "styled-components";
+import { ControlIcons } from "icons/ControlIcons";
+const PlayIcon = styled(ControlIcons.PLAY_VIDEO as AnyStyledComponent)`
+ position: relative;
+ top: 10px;
+ cursor: pointer;
+ &:hover {
+ svg {
+ path {
+ fill: ${Colors.POMEGRANATE};
+ }
+ }
+ }
+`;
+
+const PlayerWrapper = styled.div` import React, { Ref } from "react";
+ width: 600px;
+ height: 400px;
+`;
+
+const PopoverVideo = (props: VideoComponentProps) => {
+ return (
+
e.stopPropagation()}>
+
+
+
+
+
+
+
+ );
+};
+
+export default PopoverVideo;
diff --git a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx
index 69b51d2cb5..14e379d213 100644
--- a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx
+++ b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx
@@ -15,7 +15,7 @@ import {
Condition,
} from "widgets/TableWidget";
import { isString } from "lodash";
-import VideoComponent from "components/designSystems/appsmith/VideoComponent";
+import PopoverVideo from "components/designSystems/appsmith/PopoverVideo";
import Button from "components/editorComponents/Button";
import AutoToolTipComponent from "components/designSystems/appsmith/AutoToolTipComponent";
import TableColumnMenuPopup from "./TableColumnMenu";
@@ -505,7 +505,7 @@ export const renderCell = (
} else if (isString(value) && youtubeRegex.test(value)) {
return (
-
+
);
} else {
diff --git a/app/client/src/components/designSystems/appsmith/VideoComponent.tsx b/app/client/src/components/designSystems/appsmith/VideoComponent.tsx
index 08576ec847..414475c0b3 100644
--- a/app/client/src/components/designSystems/appsmith/VideoComponent.tsx
+++ b/app/client/src/components/designSystems/appsmith/VideoComponent.tsx
@@ -1,73 +1,72 @@
-import React from "react";
import ReactPlayer from "react-player";
-import {
- Popover,
- PopoverInteractionKind,
- PopoverPosition,
-} from "@blueprintjs/core";
-import { ControlIcons } from "icons/ControlIcons";
-import styled, { AnyStyledComponent } from "styled-components";
-import { Colors } from "constants/Colors";
-
-const PlayerWrapper = styled.div`
- width: 600px;
- height: 400px;
-`;
-
-const PlayIcon = styled(ControlIcons.PLAY_VIDEO as AnyStyledComponent)`
- position: relative;
- top: 10px;
- &:hover {
- svg {
- path {
- fill: ${Colors.POMEGRANATE};
- }
- }
- }
-`;
-
-interface VideoComponentProps {
- url: string;
+import React, { Ref } from "react";
+import styled from "styled-components";
+import { ENTER_VIDEO_URL } from "constants/messages";
+export interface VideoComponentProps {
+ url?: string;
+ autoplay?: boolean;
+ controls?: boolean;
+ onStart?: () => void;
+ onPlay?: () => void;
+ onPause?: () => void;
+ onEnded?: () => void;
+ onReady?: () => void;
+ onProgress?: () => void;
+ onSeek?: () => void;
+ onError?: () => void;
+ player?: Ref;
}
-const VideoComponent = (props: VideoComponentProps) => {
- return (
- e.stopPropagation()}>
-
-
-
-
-
-
-
- );
-};
+const ErrorContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+`;
-export default VideoComponent;
+const Error = styled.span``;
+
+export default function VideoComponent(props: VideoComponentProps) {
+ const {
+ url,
+ autoplay,
+ controls,
+ onStart,
+ onPlay,
+ onPause,
+ onEnded,
+ onReady,
+ onProgress,
+ onSeek,
+ onError,
+ player,
+ } = props;
+ return (
+ <>
+ {url ? (
+
+ ) : (
+
+ {ENTER_VIDEO_URL}
+
+ )}
+ >
+ );
+}
diff --git a/app/client/src/constants/ActionConstants.tsx b/app/client/src/constants/ActionConstants.tsx
index b84e46646d..88a5697d16 100644
--- a/app/client/src/constants/ActionConstants.tsx
+++ b/app/client/src/constants/ActionConstants.tsx
@@ -37,6 +37,10 @@ export enum EventType {
ON_MARKER_CLICK = "ON_MARKER_CLICK",
ON_CREATE_MARKER = "ON_CREATE_MARKER",
ON_TAB_CHANGE = "ON_TAB_CHANGE",
+ ON_VIDEO_START = "ON_VIDEO_START",
+ ON_VIDEO_END = "ON_VIDEO_END",
+ ON_VIDEO_PLAY = "ON_VIDEO_PLAY",
+ ON_VIDEO_PAUSE = "ON_VIDEO_PAUSE",
}
export type ActionType =
diff --git a/app/client/src/constants/FieldExpectedValue.ts b/app/client/src/constants/FieldExpectedValue.ts
index c9ac6d44fc..6867a4ddec 100644
--- a/app/client/src/constants/FieldExpectedValue.ts
+++ b/app/client/src/constants/FieldExpectedValue.ts
@@ -33,6 +33,11 @@ const FIELD_VALUES: Record<
// onRowSelected: "Function Call",
// onPageChange: "Function Call",
},
+ VIDEO_WIDGET: {
+ url: "string",
+ autoPlay: "boolean",
+ isVisible: "boolean",
+ },
IMAGE_WIDGET: {
image: "string",
defaultImage: "string",
diff --git a/app/client/src/constants/HelpConstants.ts b/app/client/src/constants/HelpConstants.ts
index aad2817853..2d460d0e15 100644
--- a/app/client/src/constants/HelpConstants.ts
+++ b/app/client/src/constants/HelpConstants.ts
@@ -31,6 +31,10 @@ export const HelpMap = {
path: "/widget-reference/table",
searchKey: "Table",
},
+ VIDEO_WIDGET: {
+ path: "/widget-reference/video",
+ searchKey: "Video",
+ },
DROP_DOWN_WIDGET: {
path: "/widget-reference/dropdown",
searchKey: "Dropdown",
diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx
index 744f83f3e3..a95e62d5ce 100644
--- a/app/client/src/constants/WidgetConstants.tsx
+++ b/app/client/src/constants/WidgetConstants.tsx
@@ -19,6 +19,7 @@ export enum WidgetTypes {
CANVAS_WIDGET = "CANVAS_WIDGET",
ICON_WIDGET = "ICON_WIDGET",
FILE_PICKER_WIDGET = "FILE_PICKER_WIDGET",
+ VIDEO_WIDGET = "VIDEO_WIDGET",
}
export type WidgetType = keyof typeof WidgetTypes;
diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts
index 616a46fc5f..1fa70ebae7 100644
--- a/app/client/src/constants/messages.ts
+++ b/app/client/src/constants/messages.ts
@@ -13,6 +13,8 @@ export const NAME_SPACE_ERROR = "Name must not have spaces";
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 FORM_VALIDATION_EMPTY_PASSWORD = "Please enter the password";
export const FORM_VALIDATION_PASSWORD_RULE =
"Please provide a password with a minimum of 6 characters";
diff --git a/app/client/src/icons/WidgetIcons.tsx b/app/client/src/icons/WidgetIcons.tsx
index e4c85b4f71..7eaa30f489 100644
--- a/app/client/src/icons/WidgetIcons.tsx
+++ b/app/client/src/icons/WidgetIcons.tsx
@@ -6,6 +6,7 @@ import { ReactComponent as CollapseIcon } from "assets/icons/widget/collapse.svg
import { ReactComponent as ContainerIcon } from "assets/icons/widget/container.svg";
import { ReactComponent as DatePickerIcon } from "assets/icons/widget/datepicker.svg";
import { ReactComponent as TableIcon } from "assets/icons/widget/table.svg";
+import { ReactComponent as VideoIcon } from "assets/icons/widget/video.svg";
import { ReactComponent as DropDownIcon } from "assets/icons/widget/dropdown.svg";
import { ReactComponent as CheckboxIcon } from "assets/icons/widget/checkbox.svg";
import { ReactComponent as RadioGroupIcon } from "assets/icons/widget/radio.svg";
@@ -60,6 +61,11 @@ export const WidgetIcons: {
),
+ VIDEO_WIDGET: (props: IconProps) => (
+
+
+
+ ),
DROP_DOWN_WIDGET: (props: IconProps) => (
diff --git a/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx
index 8e7224261e..b35cf50016 100644
--- a/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx
+++ b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx
@@ -119,6 +119,76 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
],
},
],
+ VIDEO_WIDGET: [
+ {
+ id: "17.1",
+ sectionName: "General",
+ children: [
+ {
+ id: "17.1.1",
+ propertyName: "url",
+ label: "Url",
+ controlType: "INPUT_TEXT",
+ placeholderText: "Enter url",
+ inputType: "TEXT",
+ },
+ {
+ id: "17.1.1",
+ propertyName: "autoPlay",
+ label: "autoPlay",
+ helpText: "Video will be automatically played",
+ controlType: "SWITCH",
+ isJSConvertible: true,
+ },
+ {
+ id: "17.1.2",
+ helpText: "Controls the visibility of the widget",
+ propertyName: "isVisible",
+ label: "Visible",
+ controlType: "SWITCH",
+ isJSConvertible: true,
+ },
+ ],
+ },
+ {
+ id: "17.2",
+ sectionName: "Actions",
+ children: [
+ {
+ id: "17.2.1",
+ helpText: "Triggers an action when the video starts playing",
+ propertyName: "onStart",
+ label: "onStart",
+ controlType: "ACTION_SELECTOR",
+ isJSConvertible: true,
+ },
+ {
+ id: "17.2.2",
+ helpText: "Triggers an action when the video ends",
+ propertyName: "onEnd",
+ label: "onEnd",
+ controlType: "ACTION_SELECTOR",
+ isJSConvertible: true,
+ },
+ {
+ id: "17.2.3",
+ helpText: "Triggers an action when the video is played",
+ propertyName: "onPlay",
+ label: "onPlay",
+ controlType: "ACTION_SELECTOR",
+ isJSConvertible: true,
+ },
+ {
+ id: "17.2.4",
+ helpText: "Triggers an action when the video is paused",
+ propertyName: "onPause",
+ label: "onPause",
+ controlType: "ACTION_SELECTOR",
+ isJSConvertible: true,
+ },
+ ],
+ },
+ ],
TABLE_WIDGET: [
{
id: "7.1",
diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx
index 9ee9d50089..9db7797085 100644
--- a/app/client/src/mockResponses/WidgetConfigResponse.tsx
+++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx
@@ -92,6 +92,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
widgetName: "DatePicker",
defaultDate: moment().format("DD/MM/YYYY HH:mm"),
},
+
+ VIDEO_WIDGET: {
+ rows: 7,
+ columns: 7,
+ widgetName: "Video",
+ url: "https://www.youtube.com/watch?v=mzqK0QIZRLs",
+ autoPlay: false,
+ },
TABLE_WIDGET: {
rows: 7,
columns: 8,
diff --git a/app/client/src/mockResponses/WidgetSidebarResponse.tsx b/app/client/src/mockResponses/WidgetSidebarResponse.tsx
index 6558de5b71..f831424f04 100644
--- a/app/client/src/mockResponses/WidgetSidebarResponse.tsx
+++ b/app/client/src/mockResponses/WidgetSidebarResponse.tsx
@@ -74,6 +74,11 @@ const WidgetSidebarResponse: {
widgetCardName: "Table",
key: generateReactKey(),
},
+ {
+ type: "VIDEO_WIDGET",
+ widgetCardName: "Video",
+ key: generateReactKey(),
+ },
{
type: "MAP_WIDGET",
widgetCardName: "Map",
diff --git a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx
index a75add8504..94a960cf06 100644
--- a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx
+++ b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx
@@ -25,6 +25,7 @@ import { FormButtonWidgetProps } from "widgets/FormButtonWidget";
import { MapWidgetProps } from "widgets/MapWidget";
import { ModalWidgetProps } from "widgets/ModalWidget";
import { IconWidgetProps } from "widgets/IconWidget";
+import { VideoWidgetProps } from "widgets/VideoWidget";
const initialState: WidgetConfigReducerState = WidgetConfigResponse;
@@ -57,6 +58,7 @@ export interface WidgetConfigReducerState {
WidgetConfigProps;
DATE_PICKER_WIDGET: Partial & WidgetConfigProps;
TABLE_WIDGET: Partial & WidgetConfigProps;
+ VIDEO_WIDGET: Partial & WidgetConfigProps;
DROP_DOWN_WIDGET: Partial & WidgetConfigProps;
CHECKBOX_WIDGET: Partial & WidgetConfigProps;
RADIO_GROUP_WIDGET: Partial & WidgetConfigProps;
diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx
index afa52083d3..ef15c269c8 100644
--- a/app/client/src/utils/WidgetRegistry.tsx
+++ b/app/client/src/utils/WidgetRegistry.tsx
@@ -38,6 +38,10 @@ import TableWidget, {
TableWidgetProps,
ProfiledTableWidget,
} from "widgets/TableWidget";
+import VideoWidget, {
+ VideoWidgetProps,
+ ProfiledVideoWidget,
+} from "widgets/VideoWidget";
import TabsWidget, {
TabsWidgetProps,
TabContainerWidgetProps,
@@ -204,6 +208,21 @@ export default class WidgetBuilderRegistry {
TableWidget.getDefaultPropertiesMap(),
TableWidget.getMetaPropertiesMap(),
);
+
+ WidgetFactory.registerWidgetBuilder(
+ "VIDEO_WIDGET",
+ {
+ buildWidget(widgetData: VideoWidgetProps): JSX.Element {
+ return ;
+ },
+ },
+ VideoWidget.getPropertyValidationMap(),
+ VideoWidget.getDerivedPropertiesMap(),
+ VideoWidget.getTriggerPropertyMap(),
+ VideoWidget.getDefaultPropertiesMap(),
+ VideoWidget.getMetaPropertiesMap(),
+ );
+
WidgetFactory.registerWidgetBuilder(
"FILE_PICKER_WIDGET",
{
diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts
index 619a96907b..aae763d7f5 100644
--- a/app/client/src/utils/autocomplete/EntityDefinitions.ts
+++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts
@@ -67,6 +67,13 @@ export const entityDefinitions = {
isVisible: isVisible,
searchText: "string",
}),
+ VIDEO_WIDGET: (widget: any) => ({
+ "!doc":
+ "Video widget can be used for playing a variety of URLs, including file paths, YouTube, Facebook, Twitch, SoundCloud, Streamable, Vimeo, Wistia, Mixcloud, and DailyMotion.",
+ "!url": "https://docs.appsmith.com/widget-reference/video",
+ playState: "number",
+ autoPlay: "bool",
+ }),
DROP_DOWN_WIDGET: {
"!doc":
"Dropdown is used to capture user input/s from a specified list of permitted inputs. A Dropdown can capture a single choice as well as multiple choices",
diff --git a/app/client/src/widgets/VideoWidget.tsx b/app/client/src/widgets/VideoWidget.tsx
new file mode 100644
index 0000000000..ec3b8aebea
--- /dev/null
+++ b/app/client/src/widgets/VideoWidget.tsx
@@ -0,0 +1,135 @@
+import React, { Suspense, lazy } from "react";
+import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
+import { WidgetType } from "constants/WidgetConstants";
+import { EventType } from "constants/ActionConstants";
+import { VALIDATION_TYPES } from "constants/WidgetValidation";
+import {
+ WidgetPropertyValidationType,
+ BASE_WIDGET_VALIDATION,
+} from "utils/ValidationFactory";
+import { TriggerPropertiesMap } from "utils/WidgetFactory";
+import Skeleton from "components/utils/Skeleton";
+import * as Sentry from "@sentry/react";
+import { retryPromise } from "utils/AppsmithUtils";
+import ReactPlayer from "react-player";
+
+const VideoComponent = lazy(() =>
+ retryPromise(() =>
+ import("components/designSystems/appsmith/VideoComponent"),
+ ),
+);
+
+export enum PlayState {
+ NOT_STARTED = "NOT_STARTED",
+ PAUSED = "PAUSED",
+ ENDED = "ENDED",
+ PLAYING = "PLAYING",
+}
+
+class VideoWidget extends BaseWidget {
+ private _player = React.createRef();
+ static getPropertyValidationMap(): WidgetPropertyValidationType {
+ return {
+ ...BASE_WIDGET_VALIDATION,
+ url: VALIDATION_TYPES.TEXT,
+ };
+ }
+
+ static getMetaPropertiesMap(): Record {
+ return {
+ playState: PlayState.NOT_STARTED,
+ };
+ }
+
+ static getDefaultPropertiesMap(): Record {
+ return {};
+ }
+
+ static getTriggerPropertyMap(): TriggerPropertiesMap {
+ return {
+ onStart: true,
+ onEnd: true,
+ onPlay: true,
+ onPause: true,
+ };
+ }
+
+ shouldComponentUpdate(nextProps: VideoWidgetProps) {
+ return nextProps.url !== this.props.url;
+ }
+
+ getPageView() {
+ const { url, autoPlay, onStart, onEnd, onPause, onPlay } = this.props;
+ return (
+ }>
+ {
+ this.updateWidgetMetaProperty("playState", PlayState.PLAYING);
+ if (onStart) {
+ super.executeAction({
+ dynamicString: onStart,
+ event: {
+ type: EventType.ON_VIDEO_START,
+ },
+ });
+ }
+ }}
+ onPlay={() => {
+ this.updateWidgetMetaProperty("playState", PlayState.PLAYING);
+ if (onPlay) {
+ super.executeAction({
+ dynamicString: onPlay,
+ event: {
+ type: EventType.ON_VIDEO_PLAY,
+ },
+ });
+ }
+ }}
+ onPause={() => {
+ //TODO: We do not want the pause event for onSeek or onEnd.
+ this.updateWidgetMetaProperty("playState", PlayState.PAUSED);
+ if (onPause) {
+ super.executeAction({
+ dynamicString: onPause,
+ event: {
+ type: EventType.ON_VIDEO_PAUSE,
+ },
+ });
+ }
+ }}
+ onEnded={() => {
+ this.updateWidgetMetaProperty("playState", PlayState.ENDED);
+ if (onEnd) {
+ super.executeAction({
+ dynamicString: onEnd,
+ event: {
+ type: EventType.ON_VIDEO_END,
+ },
+ });
+ }
+ }}
+ />
+
+ );
+ }
+
+ getWidgetType(): WidgetType {
+ return "VIDEO_WIDGET";
+ }
+}
+
+export interface VideoWidgetProps extends WidgetProps {
+ url: string;
+ autoPlay: boolean;
+ onStart?: string;
+ onPause?: string;
+ onPlay?: string;
+ onEnd?: string;
+}
+
+export default VideoWidget;
+export const ProfiledVideoWidget = Sentry.withProfiler(VideoWidget);