diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..fa176b433 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,35 @@ +name: CI +on: + workflow_dispatch: + push: + +jobs: + CI: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v2 + - uses: aws-actions/configure-aws-credentials@v1 + with: + role-to-assume: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} + aws-region: eu-west-1 + - uses: actions/setup-java@v3 + with: + java-version: "8" + distribution: "corretto" + - uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - name: Build Pluto lambda + run: | + ./scripts/pluto-ci.sh + - name: Build Media Atom Maker + run: | + ./scripts/app-ci.sh + - name: Compile Scala and upload artifacts to RiffRaff + run: | + ./scripts/scala-ci.sh + + diff --git a/app/controllers/Api.scala b/app/controllers/Api.scala index e0d7829e7..81219a901 100644 --- a/app/controllers/Api.scala +++ b/app/controllers/Api.scala @@ -45,15 +45,19 @@ class Api( } } - def getMediaAtoms(search: Option[String], limit: Option[Int]) = APIAuthAction { - val atoms = stores.atomListStore.getAtoms(search, limit) + def getMediaAtoms(search: Option[String], limit: Option[Int], shouldUseCreatedDateForSort: Boolean) = APIAuthAction { + val atoms = stores.atomListStore.getAtoms(search, limit, shouldUseCreatedDateForSort) Ok(Json.toJson(atoms)) } - def getMediaAtom(id: String) = APIAuthAction { + def getMediaAtom(id: String) = APIAuthAction {req => try { + val maybeCorsValue = req.headers.get("Origin").filter(_.endsWith("gutools.co.uk")) val atom = getPreviewAtom(id) - Ok(Json.toJson(MediaAtom.fromThrift(atom))) + Ok(Json.toJson(MediaAtom.fromThrift(atom))).withHeaders( + "Access-Control-Allow-Origin" -> maybeCorsValue.getOrElse(""), + "Access-Control-Allow-Credentials" -> maybeCorsValue.isDefined.toString + ) } catch { commandExceptionAsResult } diff --git a/app/controllers/VideoUIApp.scala b/app/controllers/VideoUIApp.scala index 221896cbc..6edf45a1a 100644 --- a/app/controllers/VideoUIApp.scala +++ b/app/controllers/VideoUIApp.scala @@ -1,6 +1,7 @@ package controllers +import com.gu.editorial.permissions.client.{Permission, PermissionDenied, PermissionsUser} import com.gu.media.MediaAtomMakerPermissionsProvider import com.gu.media.logging.Logging import com.gu.media.youtube.YouTubeAccess @@ -66,6 +67,7 @@ class VideoUIApp(val authActions: HMACAuthActions, conf: Configuration, awsConfi title = "Media Atom Maker", jsLocation, presenceJsLocation = clientConfig.presence.map(_.jsLocation), + pinboardJsLocation = if(permissions.pinboard) awsConfig.pinboardLoaderUrl else None, Json.toJson(clientConfig).toString(), isHotReloading, CSRF.getToken.value diff --git a/app/data/AtomListStore.scala b/app/data/AtomListStore.scala index 2a2f49256..b717bcfde 100644 --- a/app/data/AtomListStore.scala +++ b/app/data/AtomListStore.scala @@ -11,18 +11,18 @@ import model.{MediaAtomList, MediaAtomSummary} import play.api.libs.json.{JsArray, JsValue} trait AtomListStore { - def getAtoms(search: Option[String], limit: Option[Int]): MediaAtomList + def getAtoms(search: Option[String], limit: Option[Int], shouldUseCreatedDateForSort: Boolean): MediaAtomList } class CapiBackedAtomListStore(capi: CapiAccess) extends AtomListStore { - override def getAtoms(search: Option[String], limit: Option[Int]): MediaAtomList = { + override def getAtoms(search: Option[String], limit: Option[Int], shouldUseCreatedDateForSort: Boolean): MediaAtomList = { // CAPI max page size is 200 val cappedLimit: Option[Int] = limit.map(Math.min(200, _)) val base: Map[String, String] = Map( "types" -> "media", "order-by" -> "newest" - ) + ) ++ (if(shouldUseCreatedDateForSort) Map("order-date" -> "first-publication") else Map.empty) val baseWithSearch = search match { case Some(q) => base ++ Map( @@ -76,7 +76,7 @@ class CapiBackedAtomListStore(capi: CapiAccess) extends AtomListStore { } class DynamoBackedAtomListStore(store: PreviewDynamoDataStore) extends AtomListStore { - override def getAtoms(search: Option[String], limit: Option[Int]): MediaAtomList = { + override def getAtoms(search: Option[String], limit: Option[Int], shouldUseCreatedDateForSort: Boolean): MediaAtomList = { // We must filter the entire list of atoms rather than use Dynamo limit to ensure stable iteration order. // Without it, the front page will shuffle around when clicking the Load More button. store.listAtoms match { @@ -84,12 +84,16 @@ class DynamoBackedAtomListStore(store: PreviewDynamoDataStore) extends AtomListS AtomDataStoreError(err.msg) case Right(atoms) => - def created(atom: MediaAtom) = atom.contentChangeDetails.created.map(_.date.getMillis) + def sortField(atom: MediaAtom) = + if(shouldUseCreatedDateForSort) + atom.contentChangeDetails.created + else + atom.contentChangeDetails.lastModified val mediaAtoms = atoms .map(MediaAtom.fromThrift) .toList - .sortBy(created) + .sortBy(sortField(_).map(_.date.getMillis)) .reverse // newest atoms first val filteredAtoms = search match { diff --git a/app/model/commands/PublishAtomCommand.scala b/app/model/commands/PublishAtomCommand.scala index b3e090007..3bc36db3c 100644 --- a/app/model/commands/PublishAtomCommand.scala +++ b/app/model/commands/PublishAtomCommand.scala @@ -134,16 +134,15 @@ case class PublishAtomCommand( contentChangeDetails = atom.contentChangeDetails.copy( published = changeRecord, lastModified = changeRecord, - revision = atom.contentChangeDetails.revision + 1, scheduledLaunch = None, embargo = None ) ) AuditMessage(id, "Publish", getUsername(user)).logMessage() - UpdateAtomCommand(id, updatedAtom, stores, user, awsConfig).process() + val updatedAtomToPublish = UpdateAtomCommand(id, updatedAtom, stores, user, awsConfig).process() - val publishedAtom = publishAtomToLive(updatedAtom) + val publishedAtom = publishAtomToLive(updatedAtomToPublish) updateInactiveAssets(publishedAtom) publishedAtom } diff --git a/app/util/AWS.scala b/app/util/AWS.scala index 747a49360..1d9f36b41 100644 --- a/app/util/AWS.scala +++ b/app/util/AWS.scala @@ -29,6 +29,7 @@ class AWSConfig(override val config: Config, override val credentials: AwsCreden .withCredentials(credentials.instance) .build() + lazy val pinboardLoaderUrl = getString("panda.domain").map(domain => s"https://pinboard.$domain/pinboard.loader.js") lazy val composerUrl = getMandatoryString("flexible.url") lazy val workflowUrl = getMandatoryString("workflow.url") lazy val viewerUrl = getMandatoryString("viewer.url") diff --git a/app/util/ThumbnailGenerator.scala b/app/util/ThumbnailGenerator.scala index dcb40dec5..7b3b4f56b 100644 --- a/app/util/ThumbnailGenerator.scala +++ b/app/util/ThumbnailGenerator.scala @@ -1,7 +1,7 @@ package util import java.awt.RenderingHints -import java.awt.image.BufferedImage +import java.awt.image.{BufferedImage, ColorConvertOp} import java.io._ import java.net.URL import com.google.api.client.http.InputStreamContent @@ -24,8 +24,11 @@ case class ThumbnailGenerator(logoFile: File) extends Logging { .maxBy(_.size.get) } - private def imageAssetToBufferedImage(imageAsset: ImageAsset): BufferedImage = - ImageIO.read(new URL(imageAsset.file)) + private def imageAssetToBufferedImage(imageAsset: ImageAsset): BufferedImage = { + val image = ImageIO.read(new URL(imageAsset.file)) + val rgbImage = new BufferedImage(image.getWidth, image.getHeight, BufferedImage.TYPE_3BYTE_BGR) + new ColorConvertOp(null).filter(image, rgbImage) + } private def overlayImages(bgImage: BufferedImage, bgImageMimeType: String, atomId: String): ByteArrayInputStream = { val logoWidth: Double = List(bgImage.getWidth() / 3.0, logo.getWidth()).min diff --git a/app/views/VideoUIApp/app.scala.html b/app/views/VideoUIApp/app.scala.html index 0289a6a05..7e7ecfca8 100644 --- a/app/views/VideoUIApp/app.scala.html +++ b/app/views/VideoUIApp/app.scala.html @@ -2,6 +2,7 @@ title: String, jsFileLocation: String, presenceJsLocation: Option[String], + pinboardJsLocation: Option[String], clientConfigJson: String, isHotReloading: Boolean, csrf: String @@ -22,5 +23,8 @@

Loading...

+ @pinboardJsLocation.map { pinboardJs => + + } } diff --git a/build.sbt b/build.sbt index 2614483a9..5ceb81b5e 100644 --- a/build.sbt +++ b/build.sbt @@ -253,8 +253,6 @@ lazy val root = (project in file("root")) (scheduler / Universal / packageBin).value -> s"${(scheduler / name).value}/${(scheduler / Universal / packageBin).value.getName}", (app / baseDirectory).value / "pluto-message-ingestion/target/pluto-message-ingestion.zip" -> "pluto-message-ingestion/pluto-message-ingestion.zip", (app / baseDirectory).value / "conf/riff-raff.yaml" -> "riff-raff.yaml", - (app / baseDirectory).value / "fluentbit/td-agent-bit.conf" -> "media-atom-maker/fluentbit/td-agent-bit.conf", - (app / baseDirectory).value / "fluentbit/parsers.conf" -> "media-atom-maker/fluentbit/parsers.conf", (uploader / Compile / resourceManaged).value / "media-atom-pipeline.yaml" -> "media-atom-pipeline-cloudformation/media-atom-pipeline.yaml" ) ) diff --git a/cloudformation/media-atom-maker-dev.yml b/cloudformation/media-atom-maker-dev.yml index d76b7b273..ee3a728a7 100644 --- a/cloudformation/media-atom-maker-dev.yml +++ b/cloudformation/media-atom-maker-dev.yml @@ -96,7 +96,7 @@ Resources: Type: "AWS::IAM::AccessKey" Properties: UserName: {"Ref": "MediaAtomUser"} - Serial: 3 + Serial: 4 MediaAtomsDynamoTable: Type: "AWS::DynamoDB::Table" Properties: diff --git a/common/src/main/scala/com/gu/media/Permissions.scala b/common/src/main/scala/com/gu/media/Permissions.scala index 6b8b398e1..fea103d94 100644 --- a/common/src/main/scala/com/gu/media/Permissions.scala +++ b/common/src/main/scala/com/gu/media/Permissions.scala @@ -7,7 +7,12 @@ import play.api.libs.json.Format import com.gu.pandomainauth.model.{User => PandaUser} import scala.concurrent.Future -case class Permissions(deleteAtom: Boolean, addSelfHostedAsset: Boolean, setVideosOnAllChannelsPublic: Boolean) +case class Permissions( + deleteAtom: Boolean, + addSelfHostedAsset: Boolean, + setVideosOnAllChannelsPublic: Boolean, + pinboard: Boolean +) object Permissions { implicit val format: Format[Permissions] = Jsonx.formatCaseClass[Permissions] @@ -15,17 +20,15 @@ object Permissions { val deleteAtom = Permission("delete_atom", app, defaultVal = PermissionDenied) val addSelfHostedAsset = Permission("add_self_hosted_asset", app, defaultVal = PermissionDenied) val setVideosOnAllChannelsPublic = Permission("set_videos_on_all_channels_public", app, defaultVal = PermissionDenied) + val pinboard = Permission("pinboard", "pinboard", defaultVal = PermissionDenied) } class MediaAtomMakerPermissionsProvider(stage: String, credsProvider: AWSCredentialsProvider) extends PermissionsProvider { import Permissions._ - val all = Seq(deleteAtom, addSelfHostedAsset, setVideosOnAllChannelsPublic) - val none = Permissions(deleteAtom = false, addSelfHostedAsset = false, setVideosOnAllChannelsPublic = false ) - implicit def config = PermissionsConfig( app = "media-atom-maker", - all = Seq(deleteAtom, addSelfHostedAsset, setVideosOnAllChannelsPublic), + all = Seq(deleteAtom, addSelfHostedAsset, setVideosOnAllChannelsPublic, pinboard), s3BucketPrefix = if(stage == "PROD") "PROD" else "CODE", awsCredentials = credsProvider ) @@ -34,19 +37,13 @@ class MediaAtomMakerPermissionsProvider(stage: String, credsProvider: AWSCredent deleteAtom <- hasPermission(deleteAtom, user) selfHostedMediaAtom <- hasPermission(addSelfHostedAsset, user) publicStatusPermissions <- hasPermission(setVideosOnAllChannelsPublic, user) - } yield Permissions(deleteAtom, selfHostedMediaAtom, publicStatusPermissions) - - - def getUploadPermissions(user: PandaUser): Future[Permissions] = for { - selfHostedMediaAtom <- hasPermission(addSelfHostedAsset, user) - } yield { - Permissions(deleteAtom = false, selfHostedMediaAtom, setVideosOnAllChannelsPublic = false) - } + pinboard <- hasPermission(pinboard, user) + } yield Permissions(deleteAtom, selfHostedMediaAtom, publicStatusPermissions, pinboard) def getStatusPermissions(user: PandaUser): Future[Permissions] = for { publicStatus <- hasPermission(setVideosOnAllChannelsPublic, user) } yield { - Permissions(deleteAtom = false, addSelfHostedAsset = false, publicStatus) + Permissions(deleteAtom = false, addSelfHostedAsset = false, publicStatus, pinboard = false) } private def hasPermission(permission: Permission, user: PandaUser): Future[Boolean] = { diff --git a/conf/riff-raff.yaml b/conf/riff-raff.yaml index 4f0e38901..e48470e17 100644 --- a/conf/riff-raff.yaml +++ b/conf/riff-raff.yaml @@ -12,7 +12,7 @@ deployments: app: media-atom-maker parameters: amiTags: - Recipe: editorial-tools-focal-java8-ARM + Recipe: editorial-tools-focal-java8-ARM-WITH-cdk-base AmigoStage: PROD media-atom-maker: type: autoscaling diff --git a/conf/routes b/conf/routes index 14f90a70e..1b81373f0 100644 --- a/conf/routes +++ b/conf/routes @@ -1,5 +1,5 @@ # optional limit -GET /api/atoms controllers.Api.getMediaAtoms(search: Option[String], limit: Option[Int]) +GET /api/atoms controllers.Api.getMediaAtoms(search: Option[String], limit: Option[Int], shouldUseCreatedDateForSort: Boolean?=false) POST /api/atoms controllers.Api.createMediaAtom GET /api/atoms/:id controllers.Api.getMediaAtom(id) diff --git a/docs/01-dev-setup.md b/docs/01-dev-setup.md index b4cd6cba8..684950cdd 100644 --- a/docs/01-dev-setup.md +++ b/docs/01-dev-setup.md @@ -5,7 +5,7 @@ Ensure you have the following installed: - awscli - Java 8 - nginx -- node v10+ +- node v14.18.1 - npm - yarn - nvm @@ -13,12 +13,12 @@ Ensure you have the following installed: You'll also need Janus credentials to the `media-service` account. -## Local setup +## Local setup We use a shared DEV stack, with a shared config. Fetch it by running: ```bash -./scripts/fetch-dev-config.sh +sudo ./scripts/fetch-dev-config.sh ``` There is a chance that the IAM key used for local development (media-atom-maker-DEV) has been disabled if it has not been rotated in a while. If this is the case, and you need the key, you will need to rotate the IAM key. To do this, increment the [Serial property](https://github.com/guardian/media-atom-maker/blob/ba9f87b4b3d3f3446affabc4410ea598ae130e36/cloudformation/media-atom-maker-dev.yml#L99) in the CloudFormation template, and update the stack with the new template. This will generate the new IAM key (found in the CloudFormation `Outputs` tab, under `AwsId` and `AwsSecret`), which you should update in the dev config file in S3 (under the settings `upload.accessKey` and `upload.secretKey`). diff --git a/fluentbit/parsers.conf b/fluentbit/parsers.conf deleted file mode 100644 index 700e23ec1..000000000 --- a/fluentbit/parsers.conf +++ /dev/null @@ -1,4 +0,0 @@ -# https://docs.fluentbit.io/manual/pipeline/filters/parser -[PARSER] - Name systemd_json - Format json diff --git a/fluentbit/td-agent-bit.conf b/fluentbit/td-agent-bit.conf deleted file mode 100644 index da8287d33..000000000 --- a/fluentbit/td-agent-bit.conf +++ /dev/null @@ -1,66 +0,0 @@ -[SERVICE] - Parsers_File parsers.conf - -# https://docs.fluentbit.io/manual/pipeline/inputs/systemd -[INPUT] - Name systemd - Systemd_Filter _SYSTEMD_UNIT=APP_NAME.service - Strip_Underscores true - -# https://docs.fluentbit.io/manual/pipeline/filters/record-modifier -# Drop all systemd fields - we only want the message -[FILTER] - Name record_modifier - Match * - Allowlist_key MESSAGE - -# https://docs.fluentbit.io/manual/pipeline/filters/modify -# Lowercase MESSAGE field for consistency -[FILTER] - Name modify - Match * - Rename MESSAGE message - -# https://docs.fluentbit.io/manual/pipeline/filters/parser -# Attempt to parse the message field, in case the app is logging structured data -[FILTER] - Name parser - Match * - Key_Name message - Parser systemd_json - -# https://docs.fluentbit.io/manual/pipeline/filters/multiline-stacktrace -# Attempt to group up log lines which are split over multiple lines -[FILTER] - name multiline - match * - multiline.parser java - multiline.key_content message - -# https://docs.fluentbit.io/manual/pipeline/filters/aws-metadata -# Add useful AWS metadata -[FILTER] - Name aws - Match * - az true - ec2_instance_id true - ami_id true - -# https://docs.fluentbit.io/manual/pipeline/filters/modify -# Add app identity fields -[FILTER] - Name modify - Match * - # To be replaced with the actual value by the CFN - Add app APP_NAME - Add stage STACK_STAGE - Add stack STACK_NAME - Rename ec2_instance_id instanceId - -# https://docs.fluentbit.io/manual/pipeline/outputs/kinesis -[OUTPUT] - Name kinesis_streams - Match * - region eu-west-1 - # To be replaced with the actual value by the CFN - stream STACK_STREAM diff --git a/public/video-ui/src/actions/SearchActions/updateShouldUseCreatedDateForSort.js b/public/video-ui/src/actions/SearchActions/updateShouldUseCreatedDateForSort.js new file mode 100644 index 000000000..34a6eb3d1 --- /dev/null +++ b/public/video-ui/src/actions/SearchActions/updateShouldUseCreatedDateForSort.js @@ -0,0 +1,7 @@ +export function updateShouldUseCreatedDateForSort(shouldUseCreatedDateForSort) { + return { + type: 'UPDATE_SHOULD_USE_CREATED_DATE_FOR_SORT', + shouldUseCreatedDateForSort, + receivedAt: Date.now() + }; +} diff --git a/public/video-ui/src/actions/VideoActions/getVideos.js b/public/video-ui/src/actions/VideoActions/getVideos.js index 0e4b44a57..47e322ce3 100644 --- a/public/video-ui/src/actions/VideoActions/getVideos.js +++ b/public/video-ui/src/actions/VideoActions/getVideos.js @@ -1,10 +1,11 @@ import VideosApi from '../../services/VideosApi'; -function requestVideos(search, limit) { +function requestVideos(search, limit, shouldUseCreatedDateForSort) { return { type: 'VIDEOS_GET_REQUEST', - search: search, - limit: limit, + search, + limit, + shouldUseCreatedDateForSort, receivedAt: Date.now() }; } @@ -27,10 +28,10 @@ function errorReceivingVideos(error) { }; } -export function getVideos(search, limit) { +export function getVideos(search, limit, shouldUseCreatedDateForSort) { return dispatch => { - dispatch(requestVideos(search, limit)); - return VideosApi.fetchVideos(search, limit) + dispatch(requestVideos(search, limit, shouldUseCreatedDateForSort)); + return VideosApi.fetchVideos(search, limit, shouldUseCreatedDateForSort) .then(res => { dispatch(receiveVideos(res.total, res.atoms)); }) diff --git a/public/video-ui/src/components/FormFields/RichTextField.tsx b/public/video-ui/src/components/FormFields/RichTextField.tsx index e799f52f2..385e7f199 100644 --- a/public/video-ui/src/components/FormFields/RichTextField.tsx +++ b/public/video-ui/src/components/FormFields/RichTextField.tsx @@ -12,8 +12,7 @@ type EditorProps = { fieldValue: string; derivedFrom: string; onUpdateField: (string: string) => any; - maxLength: number; - maxCharLength: number; + maxWordLength: number; editable: boolean; fieldLocation: string; fieldName: string; @@ -46,7 +45,7 @@ export default class RichTextField extends React.Component { - if (!isTooLong(value, this.props.maxLength, this.props.maxCharLength)) { + if (!isTooLong(value, this.props.maxWordLength)) { this.setState({ isTooLong: false }); diff --git a/public/video-ui/src/components/FormFields/richtext/utils/richTextHelpers.ts b/public/video-ui/src/components/FormFields/richtext/utils/richTextHelpers.ts index fcffe44ba..e010efcb6 100644 --- a/public/video-ui/src/components/FormFields/richtext/utils/richTextHelpers.ts +++ b/public/video-ui/src/components/FormFields/richtext/utils/richTextHelpers.ts @@ -21,13 +21,12 @@ export const getWords = (text: string): string[] => { .filter(_ => _.length !== 0); }; -export const isTooLong = (value: string, maxLength: number, maxCharLength: number): boolean => { +export const isTooLong = (value: string, maxWordLength: number): boolean => { const wordLength = getWords(value).reduce((length, word) => { length += word.length; return length; }, 0); return ( - wordLength > maxLength || - value.length > maxCharLength + wordLength > maxWordLength ); }; diff --git a/public/video-ui/src/components/Header.js b/public/video-ui/src/components/Header.js index 4de20c586..4c9c000d4 100644 --- a/public/video-ui/src/components/Header.js +++ b/public/video-ui/src/components/Header.js @@ -1,14 +1,15 @@ import React from 'react'; -import { Link } from 'react-router'; +import {Link} from 'react-router'; import VideoSearch from './VideoSearch/VideoSearch'; import VideoPublishBar from './VideoPublishBar/VideoPublishBar'; import VideoPublishState from './VideoPublishState/VideoPublishState'; import AdvancedActions from './Videos/AdvancedActions'; import ComposerPageCreate from './Videos/ComposerPageCreate'; import Icon from './Icon'; -import { Presence } from './Presence'; -import { canonicalVideoPageExists } from '../util/canonicalVideoPageExists'; +import {Presence} from './Presence'; +import {canonicalVideoPageExists} from '../util/canonicalVideoPageExists'; import VideoUtils from '../util/video'; +import {QUERY_PARAM_shouldUseCreatedDateForSort} from "../constants/queryParams"; export default class Header extends React.Component { state = { presence: null }; @@ -60,6 +61,37 @@ export default class Header extends React.Component { ); } + renderSortBy() { + return ( +
+ Sort by:  + +
+ ); + } + renderHeaderBack() { return (
@@ -170,6 +202,8 @@ export default class Header extends React.Component {
+ {this.renderSortBy()} +
{this.renderCreateVideo()} {this.renderHelpLink()} @@ -188,7 +222,6 @@ export default class Header extends React.Component { className="flex-grow" video={this.props.video} publishedVideo={this.props.publishedVideo} - editableFields={this.props.editableFields} saveState={this.props.saveState} videoEditOpen={this.props.videoEditOpen} updateVideoPage={this.props.updateVideoPage} diff --git a/public/video-ui/src/components/ManagedForm/ManagedField.js b/public/video-ui/src/components/ManagedForm/ManagedField.js index 3a30cfcb5..6be941a53 100644 --- a/public/video-ui/src/components/ManagedForm/ManagedField.js +++ b/public/video-ui/src/components/ManagedForm/ManagedField.js @@ -60,7 +60,8 @@ export class ManagedField extends React.Component { this.props.isRequired, this.props.isDesired, this.props.customValidation, - composerValidation + composerValidation, + this.props.maxLength, ); if (this.props.updateFormErrors) { @@ -159,7 +160,7 @@ export class ManagedField extends React.Component { hasWarning: this.hasWarning, displayPlaceholder: this.displayPlaceholder, derivedFrom: this.props.derivedFrom, - maxCharLength: this.props.maxCharLength, + maxWordLength: this.props.maxWordLength, tagType: this.props.tagType, inputPlaceholder: this.props.inputPlaceholder, tooltip: this.props.tooltip, diff --git a/public/video-ui/src/components/Presence.js b/public/video-ui/src/components/Presence.js index 5d8d0aabe..c8bc55eec 100644 --- a/public/video-ui/src/components/Presence.js +++ b/public/video-ui/src/components/Presence.js @@ -27,7 +27,7 @@ export class Presence extends React.Component { ); } - if (current) { + if (current && window.presenceClient) { this.startPresence(current, this.props.config); } } diff --git a/public/video-ui/src/components/ReactApp.js b/public/video-ui/src/components/ReactApp.js index 599eb05be..aaad033bc 100644 --- a/public/video-ui/src/components/ReactApp.js +++ b/public/video-ui/src/components/ReactApp.js @@ -32,28 +32,15 @@ class ReactApp extends React.Component { } } - updateSearchTerm = searchTerm => { - this.props.appActions.updateSearchTerm(searchTerm); - }; - - getEditableFields = () => { - const allFields = this.props.checkedFormFields; - - const editableFormFields = Object.keys( - allFields - ).reduce((fields, formName) => { - return fields.concat(Object.keys(this.props.checkedFormFields[formName])); - }, []); - return editableFormFields; - }; - render() { const showPublishedState = this.props.params.id; return (
diff --git a/public/video-ui/src/components/VideoImages/VideoImages.js b/public/video-ui/src/components/VideoImages/VideoImages.js index 1ee493b55..fa899ff3d 100644 --- a/public/video-ui/src/components/VideoImages/VideoImages.js +++ b/public/video-ui/src/components/VideoImages/VideoImages.js @@ -23,12 +23,22 @@ export default class VideoImages extends React.Component { getGridUrl(cropType) { const posterImage = this.props.video.posterImage; + const queryParam = cropType == "verticalVideo" ? + `cropType=${cropType}&customRatio=${cropType},9,16` : + `cropType=${cropType}`; + if (posterImage.assets.length > 0) { const imageGridId = getGridMediaId(posterImage); - return `${this.props.gridDomain}/images/${imageGridId}?cropType=${cropType}`; + + return `${this.props.gridDomain}/images/${imageGridId}?${queryParam}`; } - return `${this.props.gridDomain}?cropType=${cropType}`; + return `${this.props.gridDomain}?${queryParam}`; + } + + hasVerticalVideoTag() { + const tags = this.props.video.keywords || []; + return tags.includes('tone/vertical-video'); } render() { @@ -45,7 +55,7 @@ export default class VideoImages extends React.Component {
{ + const embedUrl = getStore().getState().config.youtubeEmbedUrl; + return `${embedUrl}${id}?showinfo=0&rel=0`; +} +export function YouTubeEmbed({ id, className }) { return (