From 1e6312fe815e86305ef7d452bde6f658d0d0241a Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 29 Jul 2020 18:16:28 +0200 Subject: [PATCH 01/58] feat: extended i18n capabilities to include translations bundles from blueprints --- meteor/client/ui/i18n.ts | 108 +++++++++++------- meteor/lib/api/pubsub.ts | 1 + meteor/lib/collections/TranslationsBundles.ts | 17 +++ meteor/package-lock.json | 6 +- meteor/package.json | 2 +- .../publications/translationsBundles.ts | 19 +++ meteor/server/security/translationsBundles.ts | 22 ++++ 7 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 meteor/lib/collections/TranslationsBundles.ts create mode 100644 meteor/server/publications/translationsBundles.ts create mode 100644 meteor/server/security/translationsBundles.ts diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 91f84b4fae..c24056f72b 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -2,52 +2,80 @@ import i18n, { TFunction } from 'i18next' import Backend from 'i18next-xhr-backend' import LanguageDetector from 'i18next-browser-languagedetector' import { initReactI18next } from 'react-i18next' +import { WithManagedTracker } from '../lib/reactiveData/reactiveDataHelper' +import { PubSub } from '../../lib/api/pubsub' +import { TranslationsBundles } from '../../lib/collections/TranslationsBundles' -let i18nTranslator: TFunction -const i18nInstancePromise = i18n - .use(Backend) - .use(LanguageDetector) - .use(initReactI18next) - .init( - { - fallbackLng: { - nn: ['nb', 'en'], - default: ['en'], - }, - - // have a common namespace used around the full app - ns: ['translations'], - defaultNS: 'translations', - - debug: false, - joinArrays: '\n', - - whitelist: ['en', 'nb', 'nn', 'sv'], - - keySeparator: '→', - nsSeparator: '⇒', - pluralSeparator: '⥤', - contextSeparator: '⥤', - - interpolation: { - escapeValue: false, // not needed for react!! - }, - - react: { - wait: true, - useSuspense: false, - }, - }, - (err, t) => { +const i18nOptions = { + fallbackLng: { + nn: ['nb', 'en'], + default: ['en'], + }, + + // have a common namespace used around the full app + ns: ['translations'], + defaultNS: 'translations', + + debug: false, + joinArrays: '\n', + + whitelist: ['en', 'nb', 'nn', 'sv'], + + keySeparator: '→', + nsSeparator: '⇒', + pluralSeparator: '⥤', + contextSeparator: '⥤', + + interpolation: { + escapeValue: false, // not needed for react!! + }, + + react: { + wait: true, + useSuspense: false, + }, +} + +class I18nContainer extends WithManagedTracker { + i18nInstance: typeof i18n + i18nTranslator: TFunction + + constructor() { + super() + + this.i18nInstance = i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + + this.i18nInstance.init(i18nOptions, (err: Error, t: TFunction) => { if (err) { console.error('Error initializing i18Next', err) } else { - i18nTranslator = t + this.i18nTranslator = t } - } - ) + }) + + this.subscribe(PubSub.translationsBundles, null) + this.autorun(() => { + const bundles = TranslationsBundles.find().fetch() + for (const bundle of bundles) { + this.i18nInstance.addResourceBundle( + bundle.language, + bundle.namespace || i18nOptions.defaultNS, + bundle.data, + true, + true + ) + } + }) + } +} + +const container = new I18nContainer() +const { i18nTranslator } = container -export { i18nInstancePromise, i18nTranslator } +export { i18nTranslator } /* Notes: diff --git a/meteor/lib/api/pubsub.ts b/meteor/lib/api/pubsub.ts index b21072dffb..3778da92bd 100644 --- a/meteor/lib/api/pubsub.ts +++ b/meteor/lib/api/pubsub.ts @@ -40,6 +40,7 @@ export enum PubSub { rundownLayouts = 'rundownLayouts', buckets = 'buckets', bucketAdLibPieces = 'bucketAdLibPieces', + translationsBundles = 'translationsBundles', } export function meteorSubscribe(name: PubSub, ...args: any[]): Meteor.SubscriptionHandle { diff --git a/meteor/lib/collections/TranslationsBundles.ts b/meteor/lib/collections/TranslationsBundles.ts new file mode 100644 index 0000000000..b6db7be300 --- /dev/null +++ b/meteor/lib/collections/TranslationsBundles.ts @@ -0,0 +1,17 @@ +import { TransformedCollection } from '../typings/meteor' +import { registerCollection, ProtectedString } from '../lib' + +import { TranslationsBundle as BlueprintTranslationsBundle } from 'tv-automation-sofie-blueprints-integration' +import { createMongoCollection } from './lib' + +/** A string identifying a translations bundle */ +export type TranslationsBundleId = ProtectedString<'TranslationsBundleId'> + +export interface TranslationsBundle extends BlueprintTranslationsBundle { + _id: TranslationsBundleId +} + +export const TranslationsBundles: TransformedCollection = createMongoCollection< + TranslationsBundle +>('translationBundles') +registerCollection('TranslationBundles', TranslationsBundles) diff --git a/meteor/package-lock.json b/meteor/package-lock.json index 758aabeb39..fd621fc23b 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17617,9 +17617,9 @@ } }, "tv-automation-sofie-blueprints-integration": { - "version": "2.1.0-nightly-20200701-065943-079858b.0", - "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-2.1.0-nightly-20200701-065943-079858b.0.tgz", - "integrity": "sha512-hBnDm8Hr4d4JkgA+t3bu4s8VoiZMkL2QUttiXhZ1lmyeW2fnbW86jbVivXdHwxBAXDchV1/Pi9SdXTzyW9V79Q==", + "version": "3.0.0-nightly-feat-localisation-20200729-110353-070171e.0", + "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200729-110353-070171e.0.tgz", + "integrity": "sha512-lN+VbvLoGKPX/Dj2/VGd3vDFhTzpIWCz3haVkI8w1b4izcFETPT8Eapxa1l9UGiAfT0tbzdhE4VIHXX9MYk9gQ==", "requires": { "moment": "2.22.2", "timeline-state-resolver-types": "4.0.0-nightly-20200611-075605-93ae546a.0", diff --git a/meteor/package.json b/meteor/package.json index 25f8b18d2d..7e27a7edb9 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -97,7 +97,7 @@ "soap": "^0.31.0", "superfly-timeline": "^7.3.1", "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "2.1.0-nightly-20200701-065943-079858b.0", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200729-110353-070171e.0", "underscore": "^1.10.2", "velocity-animate": "^1.5.2", "velocity-react": "^1.4.3", diff --git a/meteor/server/publications/translationsBundles.ts b/meteor/server/publications/translationsBundles.ts new file mode 100644 index 0000000000..f765c9a504 --- /dev/null +++ b/meteor/server/publications/translationsBundles.ts @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor' + +import { TranslationsBundles } from '../../lib/collections/TranslationsBundles' +import { TranslationsBundlesSecurity } from '../security/translationsBundles' +import { meteorPublish } from './lib' +import { PubSub } from '../../lib/api/pubsub' + +meteorPublish(PubSub.translationsBundles, (selector, token) => { + if (!selector) throw new Meteor.Error(400, 'selector argument missing') + const modifier = { + fields: { + code: 0, + }, + } + if (TranslationsBundlesSecurity.allowReadAccess(selector, token, this)) { + return TranslationsBundles.find(selector, modifier) + } + return null +}) diff --git a/meteor/server/security/translationsBundles.ts b/meteor/server/security/translationsBundles.ts new file mode 100644 index 0000000000..3560df7bff --- /dev/null +++ b/meteor/server/security/translationsBundles.ts @@ -0,0 +1,22 @@ +import { TranslationsBundles, TranslationsBundle } from '../../lib/collections/TranslationsBundles' + +export namespace TranslationsBundlesSecurity { + export function allowReadAccess(selector: object, token: string, context: any) { + return true + } + export function allowWriteAccess() { + return false + } +} + +TranslationsBundles.allow({ + insert(userId: string, doc: TranslationsBundle): boolean { + return false + }, + update(userId, doc, fields, modifier) { + return false + }, + remove(userId, doc) { + return false + }, +}) From cab7f79e3fd69f455c93c6c151860ee082395999 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Fri, 31 Jul 2020 12:48:17 +0200 Subject: [PATCH 02/58] feat: correctly parse translations from blueprints and store in database --- meteor/client/ui/i18n.ts | 3 ++ meteor/package-lock.json | 6 +-- meteor/package.json | 2 +- meteor/server/api/blueprints/api.ts | 17 ++++++- meteor/server/api/blueprints/http.ts | 2 + meteor/server/api/translationsBundles.ts | 44 +++++++++++++++++++ .../publications/translationsBundles.ts | 9 ++-- 7 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 meteor/server/api/translationsBundles.ts diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index c24056f72b..9aec10c98a 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -58,7 +58,9 @@ class I18nContainer extends WithManagedTracker { this.subscribe(PubSub.translationsBundles, null) this.autorun(() => { + console.debug('ManagedTracker autorun...') const bundles = TranslationsBundles.find().fetch() + console.debug(`Got ${bundles.length} bundles from database`) for (const bundle of bundles) { this.i18nInstance.addResourceBundle( bundle.language, @@ -67,6 +69,7 @@ class I18nContainer extends WithManagedTracker { true, true ) + console.debug('i18instance updated', { bundle: { lang: bundle.language, ns: bundle.namespace } }) } }) } diff --git a/meteor/package-lock.json b/meteor/package-lock.json index fd621fc23b..59d611fbf7 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17617,9 +17617,9 @@ } }, "tv-automation-sofie-blueprints-integration": { - "version": "3.0.0-nightly-feat-localisation-20200729-110353-070171e.0", - "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200729-110353-070171e.0.tgz", - "integrity": "sha512-lN+VbvLoGKPX/Dj2/VGd3vDFhTzpIWCz3haVkI8w1b4izcFETPT8Eapxa1l9UGiAfT0tbzdhE4VIHXX9MYk9gQ==", + "version": "3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0", + "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0.tgz", + "integrity": "sha512-U5lISpMUoP5XtGnj7mHcU2mB+ZIZa6Aabj+IXSTWDqsZCoLxXuKpB9jsa4p7anm6zRxL3cKYZ6CuMZQ0b2r+Ew==", "requires": { "moment": "2.22.2", "timeline-state-resolver-types": "4.0.0-nightly-20200611-075605-93ae546a.0", diff --git a/meteor/package.json b/meteor/package.json index 7e27a7edb9..1c5a5e9110 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -97,7 +97,7 @@ "soap": "^0.31.0", "superfly-timeline": "^7.3.1", "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200729-110353-070171e.0", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0", "underscore": "^1.10.2", "velocity-animate": "^1.5.2", "velocity-react": "^1.4.3", diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index 2e77704efb..101a6379e2 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -3,13 +3,18 @@ import { getCurrentTime, protectString, unprotectString, getRandomId, makePromis import { logger } from '../../logging' import { Meteor } from 'meteor/meteor' import { Blueprints, Blueprint, BlueprintId } from '../../../lib/collections/Blueprints' -import { BlueprintManifestType, SomeBlueprintManifest } from 'tv-automation-sofie-blueprints-integration' +import { + BlueprintManifestType, + SomeBlueprintManifest, + TranslationsBundle, +} from 'tv-automation-sofie-blueprints-integration' import { Match } from 'meteor/check' import { NewBlueprintAPI, BlueprintAPIMethods } from '../../../lib/api/blueprint' import { registerClassToMeteorMethods } from '../../methods' import { parseVersion, parseRange, CoreSystem, SYSTEM_ID } from '../../../lib/collections/CoreSystem' import { evalBlueprints } from './cache' import { removeSystemStatus } from '../../systemStatus/systemStatus' +import { upsertBundles } from '../translationsBundles' export function insertBlueprint(type?: BlueprintManifestType, name?: string): BlueprintId { return Blueprints.insert({ @@ -149,6 +154,16 @@ export function uploadBlueprint( newBlueprint.studioConfigManifest = blueprintManifest.studioConfigManifest } + // extract and store translations bundled with the manifest if any + logger.debug(`blueprintManifest for ${newBlueprint.name} translations`, { + translations: (blueprintManifest as any).translations, + type: typeof (blueprintManifest as any).translations, + }) + if ((blueprintManifest as any).translations) { + const translations = (blueprintManifest as any).translations as TranslationsBundle[] + upsertBundles(translations) + } + // Parse the versions, just to verify that the format is correct: parseVersion(blueprintManifest.blueprintVersion) parseVersion(blueprintManifest.integrationVersion) diff --git a/meteor/server/api/blueprints/http.ts b/meteor/server/api/blueprints/http.ts index b7ace0e200..b1b357c98e 100644 --- a/meteor/server/api/blueprints/http.ts +++ b/meteor/server/api/blueprints/http.ts @@ -23,6 +23,8 @@ PickerPOST.route('/blueprints/restore/:blueprintId', (params, req: IncomingMessa check(blueprintId, String) check(blueprintName, Match.Maybe(String)) + logger.debug(`/blueprints/restore/:${blueprintId}`) + let content = '' try { const body = req.body diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts new file mode 100644 index 0000000000..4146f20f0c --- /dev/null +++ b/meteor/server/api/translationsBundles.ts @@ -0,0 +1,44 @@ +import { TranslationsBundles, TranslationsBundleId } from '../../lib/collections/TranslationsBundles' +import { TranslationsBundle, TranslationsBundleType } from 'tv-automation-sofie-blueprints-integration' +import { getRandomId } from '../../lib/lib' +import { logger } from '../logging' + +export function upsertBundles(bundles: TranslationsBundle[]) { + for (const bundle of bundles) { + const { type, namespace, language, data } = bundle + + if (type !== TranslationsBundleType.I18NEXT) { + throw new Error(`Unknown bundle type ${type}`) + } + + const _id = getExistingId(namespace, language) || getRandomId<'TranslationsBundleId'>() + + TranslationsBundles.upsert( + _id, + { _id, type, namespace, language, data }, + { multi: false }, + ( + err: Error, + { numberAffected, insertedId }: { numberAffected: number; insertedId?: TranslationsBundleId } + ) => { + if (!err && numberAffected) { + logger.info(`Stored translation bundle ([${insertedId || _id}]:${namespace}:${language})`) + } else { + logger.error(`Unable to store translation bundle ([${_id}]:${namespace}:${language})`, { + error: err, + }) + } + const dbCursor = TranslationsBundles.find({}) + const availableBundles = dbCursor.count() + const bundles = dbCursor.fetch() + logger.debug(`${availableBundles} bundles in database:`, { bundles }) + } + ) + } +} + +function getExistingId(namespace: string | undefined, language: string): TranslationsBundleId | null { + const bundle = TranslationsBundles.findOne({ namespace, language }) + + return bundle ? bundle._id : null +} diff --git a/meteor/server/publications/translationsBundles.ts b/meteor/server/publications/translationsBundles.ts index f765c9a504..bf825d46ad 100644 --- a/meteor/server/publications/translationsBundles.ts +++ b/meteor/server/publications/translationsBundles.ts @@ -7,13 +7,10 @@ import { PubSub } from '../../lib/api/pubsub' meteorPublish(PubSub.translationsBundles, (selector, token) => { if (!selector) throw new Meteor.Error(400, 'selector argument missing') - const modifier = { - fields: { - code: 0, - }, - } + if (TranslationsBundlesSecurity.allowReadAccess(selector, token, this)) { - return TranslationsBundles.find(selector, modifier) + return TranslationsBundles.find(selector) } + return null }) From 6ea0e2055fb1c4658351f7809b9178673136fe18 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Fri, 31 Jul 2020 17:47:16 +0200 Subject: [PATCH 03/58] chore: update blueprints-integration snapshot --- meteor/package.json | 352 ++++++++++++++++++++++---------------------- 1 file changed, 176 insertions(+), 176 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 1c5a5e9110..25eb892184 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,178 +1,178 @@ { - "name": "automation-core", - "version": "1.11.0-in-development", - "private": true, - "engines": { - "node": "12.16" - }, - "scripts": { - "start": "meteor run", - "dev": "concurrently --prefix \"[{name}]\" --names \"METEOR,TSC\" -c \"bgBlue.bold,bgGreen.bold\" \"npm run start --\" \"npm run watch-types -- --preserveWatchOutput\"", - "debug": "meteor run --inspect", - "postinstall": "meteor npm run prepareForTest", - "prepareForTest": "node ../scripts/fixTestFibers.js", - "unit": "jest", - "unitci": "jest --maxWorkers 2 --coverage", - "unitcov": "jest --coverage", - "test": "meteor npm run check-types && meteor npm run unit", - "watch": "jest --watch", - "update-snapshots": "jest --updateSnapshot", - "ci_tmp": "meteor npm run lint && meteor npm run cov", - "ci": "meteor npm run unitci && meteor npm run send-coverage", - "ci:lint": "meteor npm run check-types && meteor npm run lint", - "cov-open": "open-cli coverage/lcov-report/index.html", - "cov": "meteor npm run unitcov && meteor npm run cov-open", - "send-coverage": "codecov", - "test-i": "npm run integration-test", - "license-validate": "node ../scripts/checkLicenses.js -r --filter MIT --filter BSD --filter ISC --filter Apache --filter Unlicense --plain --border ascii", - "lint": "tslint --project tsconfig.json --config tslint.json", - "lintfix": "npm run lint -- --fix", - "quickformat": "prettier \"__mocks__/**\" --write ; prettier \"lib/**\" --write ; prettier \"server/**\" --write ; prettier \"client/**\" --write ; prettier \"*.json\" --write ; prettier \"*.js\" --write ; prettier \"*.md\" --write", - "i18n-extract-pot": "node ./scripts/extract-i18next-pot.js -f \"{./client/ui/**/*.+(ts|tsx),./lib/mediaObjects.ts}\" -o i18n/template.pot", - "i18n-compile-json": "npm run i18n-compile-json-nb & npm run i18n-compile-json-nn & npm run i18n-compile-json-sv", - "i18n-compile-json-nb": "i18next-conv -l nb -s i18n/nb.po -t public/locales/nb/translations.json --skipUntranslated", - "i18n-compile-json-nn": "i18next-conv -l nn -s i18n/nn.po -t public/locales/nn/translations.json --skipUntranslated", - "i18n-compile-json-sv": "i18next-conv -l sv -s i18n/sv.po -t public/locales/sv/translations.json --skipUntranslated", - "visualize": "meteor --production --extra-packages bundle-visualizer", - "release": "standard-version", - "prepareChangelog": "standard-version --prerelease", - "validate:dependencies": "npm audit --production && npm run license-validate", - "validate:dev-dependencies": "npm audit", - "deprecated-unit-test": "TEST_WATCH=1 meteor test --driver-package meteortesting:mocha", - "deprecated-integration-test": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", - "deprecated-test-once": "meteor test --once --driver-package meteortesting:mocha", - "check-types": "tsc --noEmit -p tsconfig.json", - "watch-types": "npm run check-types -- --watch" - }, - "dependencies": { - "@babel/runtime": "^7.10.2", - "@crello/react-lottie": "0.0.9", - "@fortawesome/fontawesome": "^1.1.8", - "@fortawesome/fontawesome-free-solid": "^5.0.13", - "@fortawesome/fontawesome-svg-core": "^1.2.28", - "@fortawesome/free-solid-svg-icons": "^5.13.0", - "@fortawesome/react-fontawesome": "^0.1.11", - "@nrk/core-icons": "^9.2.1", - "@popperjs/core": "^2.4.2", - "@slack/client": "^5.0.2", - "amqplib": "^0.5.6", - "body-parser": "^1.19.0", - "caller-module": "^1.0.4", - "classnames": "^2.2.6", - "concurrently": "^5.2.0", - "core-js": "^3.6.5", - "fast-clone": "^1.5.13", - "html-entities": "^1.3.1", - "i18next": "^19.4.5", - "i18next-browser-languagedetector": "^4.3.0", - "i18next-xhr-backend": "^3.2.2", - "immutability-helper": "^3.1.1", - "indexof": "0.0.1", - "lottie-web": "^5.6.10", - "meteor-node-stubs": "^1.0.0", - "moment": "^2.26.0", - "mos-connection": "^0.8.10", - "mousetrap": "^1.6.5", - "ntp-client": "^0.5.3", - "object-path": "^0.11.4", - "prop-types": "^15.7.2", - "query-string": "^6.13.1", - "rc-tooltip": "^4.2.1", - "react": "^16.13.1", - "react-circular-progressbar": "^2.0.3", - "react-contextmenu": "^2.14.0", - "react-datepicker": "^3.0.0", - "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", - "react-dom": "^16.13.1", - "react-escape": "0.0.8", - "react-hotkeys": "^2.0.0", - "react-i18next": "^11.6.0", - "react-intersection-observer": "^8.26.2", - "react-moment": "^0.9.7", - "react-popper": "^2.1.0", - "react-router-dom": "^5.2.0", - "react-timer-hoc": "^2.3.0", - "semver": "^7.3.2", - "soap": "^0.31.0", - "superfly-timeline": "^7.3.1", - "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0", - "underscore": "^1.10.2", - "velocity-animate": "^1.5.2", - "velocity-react": "^1.4.3", - "vm2": "^3.9.2", - "winston": "^3.2.1", - "xml2json": "^0.12.0" - }, - "devDependencies": { - "@types/amqplib": "^0.5.13", - "@types/body-parser": "^1.19.0", - "@types/classnames": "2.2.10", - "@types/css-font-loading-module": "0.0.4", - "@types/fibers": "3.1.0", - "@types/jest": "^26.0.0", - "@types/meteor": "1.4.47", - "@types/mousetrap": "^1.6.3", - "@types/node": "^12.12.47", - "@types/prop-types": "^15.7.3", - "@types/react": "^16.9.38", - "@types/react-circular-progressbar": "^1.1.0", - "@types/react-datepicker": "^3.0.0", - "@types/react-dom": "^16.9.8", - "@types/react-i18next": "^8.1.0", - "@types/react-router-dom": "^5.1.5", - "@types/request": "^2.48.5", - "@types/semver": "^7.2.0", - "@types/sinon": "^9.0.4", - "@types/winston": "^2.4.4", - "@types/xml2json": "^0.11.0", - "@types/xmldom": "^0.1.29", - "@welldone-software/why-did-you-render": "^4.2.5", - "codecov": "^3.7.0", - "ejson": "^2.2.0", - "fibers-npm": "npm:fibers@4.0.3", - "glob": "^7.1.6", - "husky": "^4.2.5", - "i18next-conv": "^10.0.2", - "i18next-scanner": "^2.11.0", - "jest": "^26.0.1", - "legally": "^3.5.5", - "lint-staged": "^10.2.11", - "meteor-babel": "^7.9.0", - "meteor-promise": "0.8.7", - "mock-http": "^1.1.0", - "open-cli": "^6.0.1", - "prettier": "^1.19.1", - "simple-git": "^2.7.0", - "sinon": "^9.0.2", - "standard-version": "^8.0.2", - "ts-jest": "^26.1.0", - "tslint": "^6.1.2", - "tslint-config-prettier": "^1.18.0", - "tslint-plugin-prettier": "^2.1.0", - "typedoc": "^0.17.7", - "typescript": "~3.8.3", - "xmldom": "^0.3.0", - "yargs": "^15.4.1" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "prettier --write" - ], - "*.{ts,tsx}": [ - "npm run lintfix --" - ] - }, - "meteor": { - "mainModule": { - "client": "client/main.tsx", - "server": "server/main.ts" - } - } + "name": "automation-core", + "version": "1.11.0-in-development", + "private": true, + "engines": { + "node": "12.16" + }, + "scripts": { + "start": "meteor run", + "dev": "concurrently --prefix \"[{name}]\" --names \"METEOR,TSC\" -c \"bgBlue.bold,bgGreen.bold\" \"npm run start --\" \"npm run watch-types -- --preserveWatchOutput\"", + "debug": "meteor run --inspect", + "postinstall": "meteor npm run prepareForTest", + "prepareForTest": "node ../scripts/fixTestFibers.js", + "unit": "jest", + "unitci": "jest --maxWorkers 2 --coverage", + "unitcov": "jest --coverage", + "test": "meteor npm run check-types && meteor npm run unit", + "watch": "jest --watch", + "update-snapshots": "jest --updateSnapshot", + "ci_tmp": "meteor npm run lint && meteor npm run cov", + "ci": "meteor npm run unitci && meteor npm run send-coverage", + "ci:lint": "meteor npm run check-types && meteor npm run lint", + "cov-open": "open-cli coverage/lcov-report/index.html", + "cov": "meteor npm run unitcov && meteor npm run cov-open", + "send-coverage": "codecov", + "test-i": "npm run integration-test", + "license-validate": "node ../scripts/checkLicenses.js -r --filter MIT --filter BSD --filter ISC --filter Apache --filter Unlicense --plain --border ascii", + "lint": "tslint --project tsconfig.json --config tslint.json", + "lintfix": "npm run lint -- --fix", + "quickformat": "prettier \"__mocks__/**\" --write ; prettier \"lib/**\" --write ; prettier \"server/**\" --write ; prettier \"client/**\" --write ; prettier \"*.json\" --write ; prettier \"*.js\" --write ; prettier \"*.md\" --write", + "i18n-extract-pot": "node ./scripts/extract-i18next-pot.js -f \"{./client/ui/**/*.+(ts|tsx),./lib/mediaObjects.ts}\" -o i18n/template.pot", + "i18n-compile-json": "npm run i18n-compile-json-nb & npm run i18n-compile-json-nn & npm run i18n-compile-json-sv", + "i18n-compile-json-nb": "i18next-conv -l nb -s i18n/nb.po -t public/locales/nb/translations.json --skipUntranslated", + "i18n-compile-json-nn": "i18next-conv -l nn -s i18n/nn.po -t public/locales/nn/translations.json --skipUntranslated", + "i18n-compile-json-sv": "i18next-conv -l sv -s i18n/sv.po -t public/locales/sv/translations.json --skipUntranslated", + "visualize": "meteor --production --extra-packages bundle-visualizer", + "release": "standard-version", + "prepareChangelog": "standard-version --prerelease", + "validate:dependencies": "npm audit --production && npm run license-validate", + "validate:dev-dependencies": "npm audit", + "deprecated-unit-test": "TEST_WATCH=1 meteor test --driver-package meteortesting:mocha", + "deprecated-integration-test": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha", + "deprecated-test-once": "meteor test --once --driver-package meteortesting:mocha", + "check-types": "tsc --noEmit -p tsconfig.json", + "watch-types": "npm run check-types -- --watch" + }, + "dependencies": { + "@babel/runtime": "^7.10.2", + "@crello/react-lottie": "0.0.9", + "@fortawesome/fontawesome": "^1.1.8", + "@fortawesome/fontawesome-free-solid": "^5.0.13", + "@fortawesome/fontawesome-svg-core": "^1.2.28", + "@fortawesome/free-solid-svg-icons": "^5.13.0", + "@fortawesome/react-fontawesome": "^0.1.11", + "@nrk/core-icons": "^9.2.1", + "@popperjs/core": "^2.4.2", + "@slack/client": "^5.0.2", + "amqplib": "^0.5.6", + "body-parser": "^1.19.0", + "caller-module": "^1.0.4", + "classnames": "^2.2.6", + "concurrently": "^5.2.0", + "core-js": "^3.6.5", + "fast-clone": "^1.5.13", + "html-entities": "^1.3.1", + "i18next": "^19.4.5", + "i18next-browser-languagedetector": "^4.3.0", + "i18next-xhr-backend": "^3.2.2", + "immutability-helper": "^3.1.1", + "indexof": "0.0.1", + "lottie-web": "^5.6.10", + "meteor-node-stubs": "^1.0.0", + "moment": "^2.26.0", + "mos-connection": "^0.8.10", + "mousetrap": "^1.6.5", + "ntp-client": "^0.5.3", + "object-path": "^0.11.4", + "prop-types": "^15.7.2", + "query-string": "^6.13.1", + "rc-tooltip": "^4.2.1", + "react": "^16.13.1", + "react-circular-progressbar": "^2.0.3", + "react-contextmenu": "^2.14.0", + "react-datepicker": "^3.0.0", + "react-dnd": "^11.1.3", + "react-dnd-html5-backend": "^11.1.3", + "react-dom": "^16.13.1", + "react-escape": "0.0.8", + "react-hotkeys": "^2.0.0", + "react-i18next": "^11.6.0", + "react-intersection-observer": "^8.26.2", + "react-moment": "^0.9.7", + "react-popper": "^2.1.0", + "react-router-dom": "^5.2.0", + "react-timer-hoc": "^2.3.0", + "semver": "^7.3.2", + "soap": "^0.31.0", + "superfly-timeline": "^7.3.1", + "timecode": "0.0.4", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0", + "underscore": "^1.10.2", + "velocity-animate": "^1.5.2", + "velocity-react": "^1.4.3", + "vm2": "^3.9.2", + "winston": "^3.2.1", + "xml2json": "^0.12.0" + }, + "devDependencies": { + "@types/amqplib": "^0.5.13", + "@types/body-parser": "^1.19.0", + "@types/classnames": "2.2.10", + "@types/css-font-loading-module": "0.0.4", + "@types/fibers": "3.1.0", + "@types/jest": "^26.0.0", + "@types/meteor": "1.4.47", + "@types/mousetrap": "^1.6.3", + "@types/node": "^12.12.47", + "@types/prop-types": "^15.7.3", + "@types/react": "^16.9.38", + "@types/react-circular-progressbar": "^1.1.0", + "@types/react-datepicker": "^3.0.0", + "@types/react-dom": "^16.9.8", + "@types/react-i18next": "^8.1.0", + "@types/react-router-dom": "^5.1.5", + "@types/request": "^2.48.5", + "@types/semver": "^7.2.0", + "@types/sinon": "^9.0.4", + "@types/winston": "^2.4.4", + "@types/xml2json": "^0.11.0", + "@types/xmldom": "^0.1.29", + "@welldone-software/why-did-you-render": "^4.2.5", + "codecov": "^3.7.0", + "ejson": "^2.2.0", + "fibers-npm": "npm:fibers@4.0.3", + "glob": "^7.1.6", + "husky": "^4.2.5", + "i18next-conv": "^10.0.2", + "i18next-scanner": "^2.11.0", + "jest": "^26.0.1", + "legally": "^3.5.5", + "lint-staged": "^10.2.11", + "meteor-babel": "^7.9.0", + "meteor-promise": "0.8.7", + "mock-http": "^1.1.0", + "open-cli": "^6.0.1", + "prettier": "^1.19.1", + "simple-git": "^2.7.0", + "sinon": "^9.0.2", + "standard-version": "^8.0.2", + "ts-jest": "^26.1.0", + "tslint": "^6.1.2", + "tslint-config-prettier": "^1.18.0", + "tslint-plugin-prettier": "^2.1.0", + "typedoc": "^0.17.7", + "typescript": "~3.8.3", + "xmldom": "^0.3.0", + "yargs": "^15.4.1" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "*.{js,css,json,md,scss}": [ + "prettier --write" + ], + "*.{ts,tsx}": [ + "npm run lintfix --" + ] + }, + "meteor": { + "mainModule": { + "client": "client/main.tsx", + "server": "server/main.ts" + } + } } From 16471ca37c3e49cd39e30b806f30ceb31e14d262 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 4 Aug 2020 10:20:19 +0200 Subject: [PATCH 04/58] feat: use parent blueprint as namespaces for translation bundles coming from blueprint uploads --- meteor/server/api/blueprints/api.ts | 2 +- meteor/server/api/translationsBundles.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index 101a6379e2..8a640b2de2 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -161,7 +161,7 @@ export function uploadBlueprint( }) if ((blueprintManifest as any).translations) { const translations = (blueprintManifest as any).translations as TranslationsBundle[] - upsertBundles(translations) + upsertBundles(translations, newBlueprint.blueprintId) } // Parse the versions, just to verify that the format is correct: diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index 4146f20f0c..06fd262084 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -2,15 +2,17 @@ import { TranslationsBundles, TranslationsBundleId } from '../../lib/collections import { TranslationsBundle, TranslationsBundleType } from 'tv-automation-sofie-blueprints-integration' import { getRandomId } from '../../lib/lib' import { logger } from '../logging' +import { BlueprintId } from '../../lib/collections/Blueprints' -export function upsertBundles(bundles: TranslationsBundle[]) { +export function upsertBundles(bundles: TranslationsBundle[], parentBlueprintId: BlueprintId) { for (const bundle of bundles) { - const { type, namespace, language, data } = bundle + const { type, language, data } = bundle if (type !== TranslationsBundleType.I18NEXT) { throw new Error(`Unknown bundle type ${type}`) } + const namespace = (parentBlueprintId as any) as string //unwrap ProtectedString const _id = getExistingId(namespace, language) || getRandomId<'TranslationsBundleId'>() TranslationsBundles.upsert( @@ -30,7 +32,7 @@ export function upsertBundles(bundles: TranslationsBundle[]) { } const dbCursor = TranslationsBundles.find({}) const availableBundles = dbCursor.count() - const bundles = dbCursor.fetch() + const bundles = dbCursor.fetch().map(({ _id, namespace, language }) => ({ _id, namespace, language })) logger.debug(`${availableBundles} bundles in database:`, { bundles }) } ) From c8c52df2b5ede03a01d88058e1f23c0706c8af59 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 5 Aug 2020 15:46:52 +0200 Subject: [PATCH 05/58] fix: typo in translationsbundles collection name --- meteor/lib/collections/TranslationsBundles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/lib/collections/TranslationsBundles.ts b/meteor/lib/collections/TranslationsBundles.ts index b6db7be300..8da351af00 100644 --- a/meteor/lib/collections/TranslationsBundles.ts +++ b/meteor/lib/collections/TranslationsBundles.ts @@ -13,5 +13,5 @@ export interface TranslationsBundle extends BlueprintTranslationsBundle { export const TranslationsBundles: TransformedCollection = createMongoCollection< TranslationsBundle ->('translationBundles') -registerCollection('TranslationBundles', TranslationsBundles) +>('translationsBundles') +registerCollection('TranslationsBundles', TranslationsBundles) From 3d2dbea751d17511c05a6d1ab42363a87a4e12b0 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 10 Aug 2020 17:29:10 +0200 Subject: [PATCH 06/58] feat: replace plain text messages with TranslatableMessages for Notes --- .editorconfig | 1 + .../notifications/NotificationCenterPanel.tsx | 27 +++++--- .../client/lib/notifications/notifications.ts | 5 +- .../client/ui/RundownView/RundownNotifier.ts | 13 +--- meteor/client/ui/i18n.ts | 43 +++++++++++-- meteor/lib/api/TranslatableMessage.ts | 64 +++++++++++++++++++ meteor/lib/api/notes.ts | 3 +- meteor/lib/api/rundown.ts | 10 ++- meteor/lib/collections/Parts.ts | 34 ++++++---- .../server/api/blueprints/context/context.ts | 2 +- meteor/server/api/ingest/rundownInput.ts | 5 +- meteor/server/publications/_publications.ts | 1 + 12 files changed, 160 insertions(+), 48 deletions(-) create mode 100644 meteor/lib/api/TranslatableMessage.ts diff --git a/.editorconfig b/.editorconfig index 7df2782495..02b4902202 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,6 @@ [*] indent_style = tab +end_of_line = lf [*.{cs,js,ts,json}] indent_size = 4 diff --git a/meteor/client/lib/notifications/NotificationCenterPanel.tsx b/meteor/client/lib/notifications/NotificationCenterPanel.tsx index 9d4b36246e..fedd036123 100644 --- a/meteor/client/lib/notifications/NotificationCenterPanel.tsx +++ b/meteor/client/lib/notifications/NotificationCenterPanel.tsx @@ -10,6 +10,7 @@ import { sofieWarningIcon as WarningIcon } from './warningIcon' import { ContextMenuTrigger, ContextMenu, MenuItem } from 'react-contextmenu' import * as _ from 'underscore' import { SegmentId } from '../../../lib/collections/Segments' +import { translateMessage, isTranslatableMessage } from '../../../lib/api/TranslatableMessage' interface IPopUpProps { item: Notification @@ -43,6 +44,8 @@ class NotificationPopUp extends React.Component { const defaultAction: NotificationAction | undefined = defaultActions.length === 1 && allActions.length === 1 ? defaultActions[0] : undefined + const message = isTranslatableMessage(item.message) ? translateMessage(item.message) : item.message + return (
{
- {item.message} + {message} {!defaultAction && allActions.length ? (
{_.map(allActions, (action: NotificationAction, i: number) => { @@ -210,15 +213,19 @@ export const NotificationCenterPopUps = translateWithTracker Notification.compare(a, b)) } - const displayList = notifications.map((item) => ( - this.dismissNotification(item)} - showDismiss={!item.persistent || !this.props.showSnoozed} - isHighlighted={item.source === highlightedSource && item.status === highlightedLevel} - /> - )) + const displayList = notifications.map((item) => { + const message = isTranslatableMessage(item.message) ? translateMessage(item.message) : item.message + + return ( + this.dismissNotification(item)} + showDismiss={!item.persistent || !this.props.showSnoozed} + isHighlighted={item.source === highlightedSource && item.status === highlightedLevel} + /> + ) + }) return ( (this.props.showEmptyListLabel || displayList.length > 0) && ( diff --git a/meteor/client/lib/notifications/notifications.ts b/meteor/client/lib/notifications/notifications.ts index b81229db5b..b1d0198410 100644 --- a/meteor/client/lib/notifications/notifications.ts +++ b/meteor/client/lib/notifications/notifications.ts @@ -7,6 +7,7 @@ import { EventEmitter } from 'events' import { Time, ProtectedString, unprotectString, isProtectedString, protectString } from '../../../lib/lib' import { HTMLAttributes } from 'react' import { SegmentId } from '../../../lib/collections/Segments' +import { ITranslatableMessage } from '../../../lib/api/TranslatableMessage' /** * Priority level for Notifications. @@ -305,7 +306,7 @@ export const NotificationCenter = new NotificationCenter0() export class Notification extends EventEmitter { id: string | undefined status: NoticeLevel - message: string | React.ReactElement | null + message: string | React.ReactElement | ITranslatableMessage | null source: NotificationsSource persistent?: boolean timeout?: number @@ -317,7 +318,7 @@ export class Notification extends EventEmitter { constructor( id: string | ProtectedString | undefined, status: NoticeLevel, - message: string | React.ReactElement | null, + message: string | React.ReactElement | ITranslatableMessage | null, source: NotificationsSource, created?: Time, persistent?: boolean, diff --git a/meteor/client/ui/RundownView/RundownNotifier.ts b/meteor/client/ui/RundownView/RundownNotifier.ts index cf70ee7293..988e5500fb 100644 --- a/meteor/client/ui/RundownView/RundownNotifier.ts +++ b/meteor/client/ui/RundownView/RundownNotifier.ts @@ -26,7 +26,7 @@ import { Rundowns, RundownId, Rundown } from '../../../lib/collections/Rundowns' import { doModalDialog } from '../../lib/ModalDialog' import { doUserAction, UserAction } from '../../lib/userAction' // import { withTranslation, getI18n, getDefaults } from 'react-i18next' -import { i18nTranslator } from '../i18n' +import { i18nTranslator as t } from '../i18n' import { PartNote, NoteType, TrackedNote } from '../../../lib/api/notes' import { Pieces, PieceId } from '../../../lib/collections/Pieces' import { PeripheralDevicesAPI } from '../../lib/clientAPI' @@ -158,7 +158,6 @@ class RundownViewNotifier extends WithManagedTracker { } private reactiveRundownStatus(playlistId: RundownPlaylistId) { - const t = i18nTranslator let oldNoteIds: Array = [] const rRundowns = reactiveData.getRRundowns(playlistId, { @@ -271,8 +270,6 @@ class RundownViewNotifier extends WithManagedTracker { } private reactivePeripheralDeviceStatus(studioId: StudioId | undefined) { - const t = i18nTranslator - let oldDevItemIds: PeripheralDeviceId[] = [] let reactivePeripheralDevices: ReactiveVar | undefined if (studioId) { @@ -381,7 +378,6 @@ class RundownViewNotifier extends WithManagedTracker { } private reactivePartNotes(playlistId: RundownPlaylistId) { - const t = i18nTranslator let allNotesPollInterval: number let allNotesPollLock: boolean = false const NOTES_POLL_INTERVAL = BACKEND_POLL_INTERVAL @@ -474,8 +470,6 @@ class RundownViewNotifier extends WithManagedTracker { } private reactiveMediaStatus(playlistId: RundownPlaylistId, showStyleBase: ShowStyleBase, studio: Studio) { - const t = i18nTranslator - let mediaObjectsPollInterval: number let mediaObjectsPollLock: boolean = false const MEDIAOBJECTS_POLL_INTERVAL = BACKEND_POLL_INTERVAL @@ -649,7 +643,6 @@ class RundownViewNotifier extends WithManagedTracker { } private reactiveQueueStatus(studioId: StudioId, playlistId: RundownPlaylistId) { - const t = i18nTranslator let reactiveUnsentMessageCount: ReactiveVar meteorSubscribe(PubSub.externalMessageQueue, { studioId: studioId, playlistId }) reactiveUnsentMessageCount = reactiveData.getUnsentExternalMessageCount(studioId, playlistId) @@ -675,8 +668,6 @@ class RundownViewNotifier extends WithManagedTracker { } private updateVersionAndConfigStatus(playlistId: RundownPlaylistId) { - const t = i18nTranslator - // Doing the check server side, to avoid needing to subscribe to the blueprint and showStyleVariant MeteorCall.rundown .rundownPlaylistNeedsResync(playlistId) @@ -875,8 +866,6 @@ class RundownViewNotifier extends WithManagedTracker { } private makeDeviceMessage(device: PeripheralDevice): string { - const t = i18nTranslator - if (!device.connected) { return t('Device {{deviceName}} is disconnected', { deviceName: device.name }) } diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 9aec10c98a..5b3d106c32 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -38,7 +38,6 @@ const i18nOptions = { class I18nContainer extends WithManagedTracker { i18nInstance: typeof i18n - i18nTranslator: TFunction constructor() { super() @@ -50,13 +49,14 @@ class I18nContainer extends WithManagedTracker { this.i18nInstance.init(i18nOptions, (err: Error, t: TFunction) => { if (err) { - console.error('Error initializing i18Next', err) + console.error('Error initializing i18Next:', err) } else { this.i18nTranslator = t + console.debug('i18nTranslator init complete') } }) - this.subscribe(PubSub.translationsBundles, null) + this.subscribe(PubSub.translationsBundles, {}) this.autorun(() => { console.debug('ManagedTracker autorun...') const bundles = TranslationsBundles.find().fetch() @@ -73,10 +73,45 @@ class I18nContainer extends WithManagedTracker { } }) } + // return key until real translator comes online + i18nTranslator(key, ...args) { + console.debug('i18nTranslator placeholder called', { key, args }) + + if (!args[0]) { + return key + } + + if (typeof args[0] === 'string') { + return key || args[0] + } + + if (args[0].defaultValue) { + return args[0].defaultValue + } + + if (typeof key !== 'string') { + return key + } + + const options = args[0] + if (options?.replace) { + Object.assign(options, { ...options.replace }) + } + + const interpolated = String(key) + for (const placeholder of key.match(/[^{\}]+(?=})/g) || []) { + const value = options[placeholder] || placeholder + interpolated.replace(`{{${placeholder}}}`, value) + } + + return interpolated + } } const container = new I18nContainer() -const { i18nTranslator } = container +const i18nTranslator: TFunction = (...args) => { + return container.i18nTranslator(args) +} export { i18nTranslator } diff --git a/meteor/lib/api/TranslatableMessage.ts b/meteor/lib/api/TranslatableMessage.ts new file mode 100644 index 0000000000..f06e298016 --- /dev/null +++ b/meteor/lib/api/TranslatableMessage.ts @@ -0,0 +1,64 @@ +import { i18nTranslator } from '../../client/ui/i18n' + +/** + * @enum - A translatable message (i18next) + */ +export interface ITranslatableMessage { + /** the message key (we use literal strings in English as keys for now) */ + key: string + /** arguments for the message template */ + args?: { [key: string]: any } + /** namespace used */ + namespace?: string +} + +/** + * Translates a message with arguments applied. Uses the application's + * translation service (see {@link '../../client/ui/i18n'}). + * + * @param {ITranslatableMessage} translatable - the translatable to translate + * @returns the translation with arguments applied + */ +export function translateMessage(translatable: ITranslatableMessage): string { + const { key: message, args, namespace } = translatable + + return i18nTranslator(message, { ns: namespace, replace: { ...args } }) +} + +/** + * Type check predicate for the ITranslatableMessage interface + * + * @param obj the value to typecheck + * + * @returns {boolean} true if the value implements the interface, false if not + */ +export function isTranslatableMessage(obj: any): obj is ITranslatableMessage { + if (!obj) { + return false + } + + const { message, args, namespace } = obj + + if (!message || typeof message !== 'string') { + return false + } + + if (args && !checkArgs(args)) { + return false + } + + if (namespace && typeof namespace !== 'string') { + return false + } + + return true +} + +function checkArgs(args: any): args is { [key: string]: any } { + if (args === undefined || args === null) { + return false + } + + // this is good enough for object literals, which is what args essentially is + return args.constructor === Object +} diff --git a/meteor/lib/api/notes.ts b/meteor/lib/api/notes.ts index aaf0c19bbe..2e6559dbf5 100644 --- a/meteor/lib/api/notes.ts +++ b/meteor/lib/api/notes.ts @@ -2,6 +2,7 @@ import { RundownId } from '../collections/Rundowns' import { SegmentId } from '../collections/Segments' import { PartId } from '../collections/Parts' import { PieceId } from '../collections/Pieces' +import { ITranslatableMessage } from './TranslatableMessage' export enum NoteType { WARNING = 1, @@ -9,7 +10,7 @@ export enum NoteType { } export interface INoteBase { type: NoteType - message: string + message: ITranslatableMessage } export interface TrackedNote extends GenericNote { diff --git a/meteor/lib/api/rundown.ts b/meteor/lib/api/rundown.ts index df0ba1fe51..5b6962c299 100644 --- a/meteor/lib/api/rundown.ts +++ b/meteor/lib/api/rundown.ts @@ -81,9 +81,13 @@ export function runInRundownContext(rundown: Rundown, fcn: () => T, errorInfo function handleRundownContextError(rundown: Rundown, errorInformMessage: string | undefined, error: any) { rundown.appendNote({ type: NoteType.ERROR, - message: - (errorInformMessage ? errorInformMessage : 'Something went wrong when processing data this rundown.') + - `Error message: ${(error || 'N/A').toString()}`, + message: { + key: `${errorInformMessage || + 'Something went wrong when processing data this rundown.'} Error message: {{error}}`, + args: { + error: `${error || 'N/A'}`, + }, + }, origin: { name: rundown.name, }, diff --git a/meteor/lib/collections/Parts.ts b/meteor/lib/collections/Parts.ts index d536c40912..a77386525f 100644 --- a/meteor/lib/collections/Parts.ts +++ b/meteor/lib/collections/Parts.ts @@ -189,19 +189,23 @@ export class Part implements DBPart { return this.getAdLibPieces() } getInvalidReasonNotes(): Array { - return this.invalidReason - ? [ - { - type: NoteType.WARNING, - message: - this.invalidReason.title + - (this.invalidReason.description ? ': ' + this.invalidReason.description : ''), - origin: { - name: this.title, - }, - }, - ] - : [] + const notes: PartNote[] = [] + + if (this.invalidReason) { + notes.push({ + type: NoteType.WARNING, + message: { + key: `${this.invalidReason.title}${ + this.invalidReason.description ? `: ${+this.invalidReason.description}` : '' + }`, + }, + origin: { + name: this.title, + }, + }) + } + + return notes } getMinimumReactiveNotes(studio: Studio, showStyleBase: ShowStyleBase): Array { let notes: Array = [] @@ -225,7 +229,9 @@ export class Part implements DBPart { name: 'Media Check', pieceId: piece._id, }, - message: st.message || '', + message: { + key: st.message || '', + }, }) } } diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index 251f389b7a..a891560037 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -134,7 +134,7 @@ export class NotesContext extends CommonContext implements INotesContext { if (this._handleNotesExternally) { this.savedNotes.push({ type: type, - message: message, + message: { key: message }, trackingId: trackingId, }) } else { diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index f41dac1384..261efa0acb 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -541,7 +541,10 @@ function updateRundownFromIngestData( if (!dbRundownData.notes) dbRundownData.notes = [] dbRundownData.notes.push({ type: NoteType.WARNING, - message: `The Rundown was attempted to be moved out of the Playlist when it was on Air. Move it back and try again later.`, + message: { + key: + 'The Rundown was attempted to be moved out of the Playlist when it was on Air. Move it back and try again later.', + }, origin: { name: 'Data update', }, diff --git a/meteor/server/publications/_publications.ts b/meteor/server/publications/_publications.ts index 7093161347..3968d18240 100644 --- a/meteor/server/publications/_publications.ts +++ b/meteor/server/publications/_publications.ts @@ -32,4 +32,5 @@ import './showStyleVariants' import './snapshots' import './studios' import './timeline' +import './translationsBundles' import './userActionsLog' From cb4cb452e7ab8197479f0922d59ddb0e0cbe1d6b Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 12 Aug 2020 18:28:53 +0200 Subject: [PATCH 07/58] feat!: Notes now support parametrized strings (changes NotesContext#error/warning signatures) --- meteor/package-lock.json | 6 +++--- meteor/package.json | 2 +- .../server/api/blueprints/context/context.ts | 21 ++++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/meteor/package-lock.json b/meteor/package-lock.json index 59d611fbf7..0e9d764e50 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17617,9 +17617,9 @@ } }, "tv-automation-sofie-blueprints-integration": { - "version": "3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0", - "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0.tgz", - "integrity": "sha512-U5lISpMUoP5XtGnj7mHcU2mB+ZIZa6Aabj+IXSTWDqsZCoLxXuKpB9jsa4p7anm6zRxL3cKYZ6CuMZQ0b2r+Ew==", + "version": "3.0.0-nightly-feat-localisation-20200811-143459-3ddac3f.0", + "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200811-143459-3ddac3f.0.tgz", + "integrity": "sha512-wJdYlwUku8ker9aGPOslKqOsjvtKwDDTS3Dvyps0Ld96ZjBEDZaXpwEdMDLhpUs0lADqcZ2NN1AxtvwUE+mCKQ==", "requires": { "moment": "2.22.2", "timeline-state-resolver-types": "4.0.0-nightly-20200611-075605-93ae546a.0", diff --git a/meteor/package.json b/meteor/package.json index 25eb892184..d379760777 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -97,7 +97,7 @@ "soap": "^0.31.0", "superfly-timeline": "^7.3.1", "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200730-155622-28e011e.0", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200811-143459-3ddac3f.0", "underscore": "^1.10.2", "velocity-animate": "^1.5.2", "velocity-react": "^1.4.3", diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index a891560037..558a6b8f7f 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -110,16 +110,16 @@ export class NotesContext extends CommonContext implements INotesContext { this._handleNotesExternally = handleNotesExternally } /** Throw Error and display message to the user in the GUI */ - error(message: string, trackingId?: string) { + error(message: string, params?: { [key: string]: any }, trackingId?: string) { check(message, String) logger.error('Error from blueprint: ' + message) - this._pushNote(NoteType.ERROR, message, trackingId) + this._pushNote(NoteType.ERROR, message, params, trackingId) throw new Meteor.Error(500, message) } /** Save note, which will be displayed to the user in the GUI */ - warning(message: string, trackingId?: string) { + warning(message: string, params?: { [key: string]: any }, trackingId?: string) { check(message, String) - this._pushNote(NoteType.WARNING, message, trackingId) + this._pushNote(NoteType.WARNING, message, params, trackingId) } getNotes(): RawNote[] { return this.savedNotes @@ -130,11 +130,12 @@ export class NotesContext extends CommonContext implements INotesContext { set handleNotesExternally(value: boolean) { this._handleNotesExternally = value } - protected _pushNote(type: NoteType, message: string, trackingId: string | undefined) { + protected _pushNote(type: NoteType, message: string, args?: { [key: string]: any }, trackingId?: string) { if (this._handleNotesExternally) { + // TODO: get blueprintId for context to use as namespace for the message this.savedNotes.push({ type: type, - message: { key: message }, + message: { key: message, args }, trackingId: trackingId, }) } else { @@ -250,11 +251,11 @@ export class ShowStyleContext extends StudioContext implements IShowStyleContext } /** NotesContext */ - error(message: string, trackingId?: string) { - this.notesContext.error(message, trackingId) + error(message: string, params?: { [key: string]: any }, trackingId?: string) { + this.notesContext.error(message, params, trackingId) } - warning(message: string, trackingId?: string) { - this.notesContext.warning(message, trackingId) + warning(message: string, params?: { [key: string]: any }, trackingId?: string) { + this.notesContext.warning(message, params, trackingId) } getHashId(str: string, isNotUnique?: boolean) { return this.notesContext.getHashId(str, isNotUnique) From ecf69e481f3c29e9f4d59dac47700c75208a8844 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 18 Aug 2020 11:21:14 +0200 Subject: [PATCH 08/58] feat: support namespaces in NotesContext to utilize translations from blueprints --- meteor/client/ui/i18n.ts | 9 ++++++- meteor/lib/api/TranslatableMessage.ts | 6 ++--- .../server/api/blueprints/context/context.ts | 17 +++++++++---- meteor/server/api/blueprints/postProcess.ts | 7 +++++- meteor/server/api/ingest/bucketAdlibs.ts | 9 +++++-- meteor/server/api/ingest/rundownInput.ts | 24 +++++++++++++++---- meteor/server/api/playout/playout.ts | 6 ++++- 7 files changed, 62 insertions(+), 16 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 5b3d106c32..14af69c7d5 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -110,7 +110,14 @@ class I18nContainer extends WithManagedTracker { const container = new I18nContainer() const i18nTranslator: TFunction = (...args) => { - return container.i18nTranslator(args) + console.debug('i18nTranslator call', args) + // if (args[0] === `Device {{deviceName}} is disconnected`) { + // console.debug(`Got ${args[0]}, aborting`, args) + // return args[0] + // } + const result = container.i18nTranslator(args) + console.debug(`=> ${result}`) + return result } export { i18nTranslator } diff --git a/meteor/lib/api/TranslatableMessage.ts b/meteor/lib/api/TranslatableMessage.ts index f06e298016..568c8fefb6 100644 --- a/meteor/lib/api/TranslatableMessage.ts +++ b/meteor/lib/api/TranslatableMessage.ts @@ -9,7 +9,7 @@ export interface ITranslatableMessage { /** arguments for the message template */ args?: { [key: string]: any } /** namespace used */ - namespace?: string + namespaces?: Array } /** @@ -20,9 +20,9 @@ export interface ITranslatableMessage { * @returns the translation with arguments applied */ export function translateMessage(translatable: ITranslatableMessage): string { - const { key: message, args, namespace } = translatable + const { key: message, args, namespaces } = translatable - return i18nTranslator(message, { ns: namespace, replace: { ...args } }) + return i18nTranslator(message, { ns: namespaces, replace: { ...args } }) } /** diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index 558a6b8f7f..ab9c3f3547 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -61,7 +61,7 @@ import { unprotectPartInstance, PartInstance, } from '../../../../lib/collections/PartInstances' -import { Blueprints } from '../../../../lib/collections/Blueprints' +import { Blueprints, BlueprintId } from '../../../../lib/collections/Blueprints' import { ExternalMessageQueue } from '../../../../lib/collections/ExternalMessageQueue' import { extendIngestRundownCore } from '../../ingest/lib' @@ -99,15 +99,25 @@ export class NotesContext extends CommonContext implements INotesContext { private readonly _contextName: string private readonly _contextIdentifier: string private _handleNotesExternally: boolean + private _namespaces: Array private readonly savedNotes: Array = [] - constructor(contextName: string, contextIdentifier: string, handleNotesExternally: boolean) { + constructor( + contextName: string, + contextIdentifier: string, + handleNotesExternally: boolean, + blueprints?: Array + ) { super(contextIdentifier) this._contextName = contextName this._contextIdentifier = contextIdentifier /** If the notes will be handled externally (using .getNotes()), set this to true */ this._handleNotesExternally = handleNotesExternally + + if (blueprints) { + this._namespaces = blueprints.slice() + } } /** Throw Error and display message to the user in the GUI */ error(message: string, params?: { [key: string]: any }, trackingId?: string) { @@ -132,10 +142,9 @@ export class NotesContext extends CommonContext implements INotesContext { } protected _pushNote(type: NoteType, message: string, args?: { [key: string]: any }, trackingId?: string) { if (this._handleNotesExternally) { - // TODO: get blueprintId for context to use as namespace for the message this.savedNotes.push({ type: type, - message: { key: message, args }, + message: { key: message, args, namespaces: this._namespaces }, trackingId: trackingId, }) } else { diff --git a/meteor/server/api/blueprints/postProcess.ts b/meteor/server/api/blueprints/postProcess.ts index 1053260b18..fe79556a9e 100644 --- a/meteor/server/api/blueprints/postProcess.ts +++ b/meteor/server/api/blueprints/postProcess.ts @@ -229,7 +229,12 @@ export function postProcessAdLibActions( export function postProcessStudioBaselineObjects(studio: Studio, objs: TSR.TSRTimelineObjBase[]): TimelineObjRundown[] { const timelineUniqueIds: { [id: string]: true } = {} - const context = new NotesContext('studio', 'studio', false) + const context = new NotesContext( + 'studio', + 'studio', + false, + studio.blueprintId ? [unprotectString(studio.blueprintId)] : undefined + ) return postProcessTimelineObjects( context, protectString('studio'), diff --git a/meteor/server/api/ingest/bucketAdlibs.ts b/meteor/server/api/ingest/bucketAdlibs.ts index 2b02673806..ae6ea6638c 100644 --- a/meteor/server/api/ingest/bucketAdlibs.ts +++ b/meteor/server/api/ingest/bucketAdlibs.ts @@ -14,7 +14,7 @@ import { cleanUpExpectedMediaItemForBucketAdLibPiece, updateExpectedMediaItemForBucketAdLibPiece, } from '../expectedMediaItems' -import { waitForPromise } from '../../../lib/lib' +import { waitForPromise, unprotectString } from '../../../lib/lib' import { initCacheForRundownPlaylist } from '../../DatabaseCaches' export function updateBucketAdlibFromIngestData( @@ -29,7 +29,12 @@ export function updateBucketAdlibFromIngestData( studio, showStyle._id, showStyle.showStyleVariantId, - new NotesContext('Bucket Ad-Lib', 'bucket-adlib', false) + new NotesContext( + 'Bucket Ad-Lib', + 'bucket-adlib', + false, + [blueprintId, studio.blueprintId].map(unprotectString).filter((id): id is string => id !== undefined) + ) ) if (!blueprint.getAdlibItem) throw new Meteor.Error(501, "This blueprint doesn't support ingest AdLibs") const rawAdlib = blueprint.getAdlibItem(context, ingestData) diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index 261efa0acb..2a98ce982a 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -456,10 +456,15 @@ function updateRundownFromIngestData( } const showStyleBlueprint = loadShowStyleBlueprints(showStyle.base).blueprint + + const usedBlueprints = [unprotectString(studio.blueprintId), showStyleBlueprint.blueprintId].filter( + (id): id is string => id !== undefined + ) const notesContext = new NotesContext( `${showStyle.base.name}-${showStyle.variant.name}`, `showStyleBaseId=${showStyle.base._id},showStyleVariantId=${showStyle.variant._id}`, - true + true, + usedBlueprints ) const blueprintContext = new ShowStyleContext(studio, showStyle.base._id, showStyle.variant._id, notesContext) const rundownRes = showStyleBlueprint.getRundown(blueprintContext, extendedIngestRundown) @@ -611,7 +616,7 @@ function updateRundownFromIngestData( const cache = waitForPromise(initCacheForRundownPlaylist(dbPlaylist)) // Save the baseline - const rundownNotesContext = new NotesContext(dbRundown.name, `rundownId=${dbRundown._id}`, true) + const rundownNotesContext = new NotesContext(dbRundown.name, `rundownId=${dbRundown._id}`, true, usedBlueprints) const blueprintRundownContext = new RundownContext(dbRundown, rundownNotesContext, studio) logger.info(`Building baseline objects for ${dbRundown._id}...`) logger.info(`... got ${rundownRes.baseline.length} objects from baseline.`) @@ -660,7 +665,12 @@ function updateRundownFromIngestData( ingestSegment.parts = _.sortBy(ingestSegment.parts, (part) => part.rank) - const notesContext = new NotesContext(ingestSegment.name, `rundownId=${rundownId},segmentId=${segmentId}`, true) + const notesContext = new NotesContext( + ingestSegment.name, + `rundownId=${rundownId},segmentId=${segmentId}`, + true, + usedBlueprints + ) const context = new SegmentContext(dbRundown, studio, existingParts, notesContext) const res = blueprint.getSegment(context, ingestSegment) @@ -1157,7 +1167,13 @@ function updateSegmentFromIngestData( ingestSegment.parts = _.sortBy(ingestSegment.parts, (s) => s.rank) - const notesContext = new NotesContext(ingestSegment.name, `rundownId=${rundown._id},segmentId=${segmentId}`, true) + const usedBlueprints = [blueprintId, studio.blueprintId].map(unprotectString) + const notesContext = new NotesContext( + ingestSegment.name, + `rundownId=${rundown._id},segmentId=${segmentId}`, + true, + usedBlueprints + ) const context = new SegmentContext(rundown, studio, existingParts, notesContext) const res = blueprint.getSegment(context, ingestSegment) diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index a0bba69009..5b340dae86 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -1427,12 +1427,16 @@ export namespace ServerPlayoutAPI { if (!rundown) throw new Meteor.Error(501, `Current Rundown "${currentPartInstance.rundownId}" could not be found`) + const usedBlueprints = [studio.blueprintId, rundown.getShowStyleBase().blueprintId] + .map(unprotectString) + .filter((id): id is string => id !== undefined) const notesContext = new NotesContext( `${rundown.name}(${playlist.name})`, `playlist=${playlist._id},rundown=${rundown._id},currentPartInstance=${ currentPartInstance._id },execution=${getRandomId()}`, - false + false, + usedBlueprints ) const context = new ActionExecutionContext(cache, notesContext, studio, playlist, rundown) From 9fe9c547dee2411ecd964a9add3a856c33519834 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Thu, 3 Sep 2020 15:33:56 +0200 Subject: [PATCH 09/58] chore: rewrite test to fit changed NotesContext error and warning interfaces --- .../server/api/blueprints/__tests__/context.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/meteor/server/api/blueprints/__tests__/context.test.ts b/meteor/server/api/blueprints/__tests__/context.test.ts index dc6192dee4..9578ba7531 100644 --- a/meteor/server/api/blueprints/__tests__/context.test.ts +++ b/meteor/server/api/blueprints/__tests__/context.test.ts @@ -498,14 +498,18 @@ describe('Test blueprint api context', () => { // Apply mocked notesContext: ;(context as any).notesContext = fakeNotes - context.error('this is an error', 'extid1') + context.error('this is an {{error}}', { error: 'embarrasing situation' }, 'extid1') expect(fakeNotes.error).toHaveBeenCalledTimes(1) - expect(fakeNotes.error).toHaveBeenCalledWith('this is an error', 'extid1') + expect(fakeNotes.error).toHaveBeenCalledWith( + 'this is an {{error}}', + { error: 'embarrasing situation' }, + 'extid1' + ) - context.warning('this is an warning', 'extid1') + context.warning('this is an warning', {}, 'extid1') expect(fakeNotes.warning).toHaveBeenCalledTimes(1) - expect(fakeNotes.warning).toHaveBeenCalledWith('this is an warning', 'extid1') + expect(fakeNotes.warning).toHaveBeenCalledWith('this is an warning', {}, 'extid1') const hash = context.getHashId('str 1', false) expect(hash).toEqual('hashed') From 006b5918f53ef9e1ec3b2a839c4d56f864bdd28e Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Fri, 4 Sep 2020 16:56:03 +0200 Subject: [PATCH 10/58] feat: don't add empty translations bundles to the client side translator --- meteor/server/security/translationsBundles.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meteor/server/security/translationsBundles.ts b/meteor/server/security/translationsBundles.ts index 3560df7bff..815125587d 100644 --- a/meteor/server/security/translationsBundles.ts +++ b/meteor/server/security/translationsBundles.ts @@ -1,22 +1,22 @@ import { TranslationsBundles, TranslationsBundle } from '../../lib/collections/TranslationsBundles' export namespace TranslationsBundlesSecurity { - export function allowReadAccess(selector: object, token: string, context: any) { + export function allowReadAccess(selector: object, token: string, context: any): boolean { return true } - export function allowWriteAccess() { + export function allowWriteAccess(): boolean { return false } } TranslationsBundles.allow({ - insert(userId: string, doc: TranslationsBundle): boolean { + insert(): boolean { return false }, - update(userId, doc, fields, modifier) { + update(): boolean { return false }, - remove(userId, doc) { + remove(): boolean { return false }, }) From b9c0b10cba5c647a1a2ed00f3a88fa5567b50f73 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Fri, 4 Sep 2020 17:01:00 +0200 Subject: [PATCH 11/58] fix: parametrized strings would sometimes cause an exception in the i18n translator --- meteor/client/ui/i18n.ts | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 14af69c7d5..4a22e6612c 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -62,14 +62,19 @@ class I18nContainer extends WithManagedTracker { const bundles = TranslationsBundles.find().fetch() console.debug(`Got ${bundles.length} bundles from database`) for (const bundle of bundles) { - this.i18nInstance.addResourceBundle( - bundle.language, - bundle.namespace || i18nOptions.defaultNS, - bundle.data, - true, - true - ) - console.debug('i18instance updated', { bundle: { lang: bundle.language, ns: bundle.namespace } }) + if (Object.keys(bundle.data).length > 0) { + this.i18nInstance.addResourceBundle( + bundle.language, + bundle.namespace || i18nOptions.defaultNS, + bundle.data, + true, + true + ) + console.debug('i18instance updated', { bundle: { lang: bundle.language, ns: bundle.namespace } }) + } else { + //TODO: remove, debug use only + console.debug(`Skipped bundle, no translations`, { bundle }) + } } }) } @@ -109,15 +114,8 @@ class I18nContainer extends WithManagedTracker { } const container = new I18nContainer() -const i18nTranslator: TFunction = (...args) => { - console.debug('i18nTranslator call', args) - // if (args[0] === `Device {{deviceName}} is disconnected`) { - // console.debug(`Got ${args[0]}, aborting`, args) - // return args[0] - // } - const result = container.i18nTranslator(args) - console.debug(`=> ${result}`) - return result +const i18nTranslator: TFunction = (key, options) => { + return container.i18nTranslator(key, options) } export { i18nTranslator } From 923a4cb31dcdc8b64c4ffb8d21997736e6a3defe Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 7 Sep 2020 15:09:47 +0200 Subject: [PATCH 12/58] chore: use API function for unwrapping blueprint id in translation bundles API --- meteor/server/api/translationsBundles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index 06fd262084..501a941ed7 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -1,6 +1,6 @@ import { TranslationsBundles, TranslationsBundleId } from '../../lib/collections/TranslationsBundles' import { TranslationsBundle, TranslationsBundleType } from 'tv-automation-sofie-blueprints-integration' -import { getRandomId } from '../../lib/lib' +import { getRandomId, unprotectString } from '../../lib/lib' import { logger } from '../logging' import { BlueprintId } from '../../lib/collections/Blueprints' @@ -12,7 +12,7 @@ export function upsertBundles(bundles: TranslationsBundle[], parentBlueprintId: throw new Error(`Unknown bundle type ${type}`) } - const namespace = (parentBlueprintId as any) as string //unwrap ProtectedString + const namespace = unprotectString(parentBlueprintId) const _id = getExistingId(namespace, language) || getRandomId<'TranslationsBundleId'>() TranslationsBundles.upsert( From 8868a0c0d36df6a31346ac6d299744c888aa77d3 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 9 Sep 2020 17:52:14 +0200 Subject: [PATCH 13/58] fix: ITranslatableMessage typechecker predicate had multiple bugs --- meteor/lib/api/TranslatableMessage.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/meteor/lib/api/TranslatableMessage.ts b/meteor/lib/api/TranslatableMessage.ts index 568c8fefb6..99597da881 100644 --- a/meteor/lib/api/TranslatableMessage.ts +++ b/meteor/lib/api/TranslatableMessage.ts @@ -1,4 +1,4 @@ -import { i18nTranslator } from '../../client/ui/i18n' +import { TFunction } from 'i18next' /** * @enum - A translatable message (i18next) @@ -13,13 +13,15 @@ export interface ITranslatableMessage { } /** - * Translates a message with arguments applied. Uses the application's - * translation service (see {@link '../../client/ui/i18n'}). + * Convenience function to translate a message using a supplied translation function. * * @param {ITranslatableMessage} translatable - the translatable to translate + * @param {TFunction} i18nTranslator - the translation function to use * @returns the translation with arguments applied */ -export function translateMessage(translatable: ITranslatableMessage): string { +export function translateMessage(translatable: ITranslatableMessage, i18nTranslator: TFunction): string { + // the reason for injecting the translation function rather than including the inited function from i18n.ts + // is to avoid a situation where this is accidentally used from the server side causing an error const { key: message, args, namespaces } = translatable return i18nTranslator(message, { ns: namespaces, replace: { ...args } }) @@ -37,9 +39,9 @@ export function isTranslatableMessage(obj: any): obj is ITranslatableMessage { return false } - const { message, args, namespace } = obj + const { key, args, namespaces } = obj - if (!message || typeof message !== 'string') { + if (!key || typeof key !== 'string') { return false } @@ -47,7 +49,7 @@ export function isTranslatableMessage(obj: any): obj is ITranslatableMessage { return false } - if (namespace && typeof namespace !== 'string') { + if (namespaces && !Array.isArray(namespaces) && namespaces.find((ns) => typeof ns !== 'string')) { return false } From 4d47369d149b70d4b18ca84b327753f5772d963f Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 9 Sep 2020 18:15:13 +0200 Subject: [PATCH 14/58] feat: add support for TranslatableMessages in notification center --- .../notifications/NotificationCenterPanel.tsx | 31 ++++++++----- .../client/ui/RundownView/RundownNotifier.tsx | 43 ++++++++++--------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/meteor/client/lib/notifications/NotificationCenterPanel.tsx b/meteor/client/lib/notifications/NotificationCenterPanel.tsx index 34f92e794b..5171f33ebe 100644 --- a/meteor/client/lib/notifications/NotificationCenterPanel.tsx +++ b/meteor/client/lib/notifications/NotificationCenterPanel.tsx @@ -11,6 +11,7 @@ import { SegmentId } from '../../../lib/collections/Segments' import { translateMessage, isTranslatableMessage } from '../../../lib/api/TranslatableMessage' import { CriticalIcon, WarningIcon, CollapseChevrons, InformationIcon } from '../notificationIcons' import update from 'immutability-helper' +import { i18nTranslator } from '../../ui/i18n' interface IPopUpProps { id?: string @@ -41,13 +42,13 @@ class NotificationPopUp extends React.Component { render() { const { item } = this.props - const defaultActions: NotificationAction[] = _.filter(item.actions || [], (i) => i.type === 'default') const allActions: NotificationAction[] = item.actions || [] + const defaultActions: NotificationAction[] = allActions.filter((action) => action.type === 'default') const defaultAction: NotificationAction | undefined = defaultActions.length === 1 && allActions.length === 1 ? defaultActions[0] : undefined - const message = isTranslatableMessage(item.message) ? translateMessage(item.message) : item.message + const message = isTranslatableMessage(item.message) ? translateMessage(item.message, i18nTranslator) : item.message return (
{ - return item.id - ? item.id - : item.created + - (typeof item.message === 'string' - ? item.message - : item.message === null - ? 'null' - : `jsx_${btoa(JSON.stringify(item.message))}` - ).toString() + if (item.id) { + return item.id + } + + if (item.message === null) { + return `${item.created}null` + } + + if (typeof item.message === 'string') { + return `${item.created}${item.message}` + } + + if (isTranslatableMessage(item.message)) { + return `${item.created}${translateMessage(item.message, this.props.t)}` + } + + return `${item.created}$jsx_${btoa(JSON.stringify(item.message))}` } render() { diff --git a/meteor/client/ui/RundownView/RundownNotifier.tsx b/meteor/client/ui/RundownView/RundownNotifier.tsx index 8acf18f272..33a7b80bf7 100644 --- a/meteor/client/ui/RundownView/RundownNotifier.tsx +++ b/meteor/client/ui/RundownView/RundownNotifier.tsx @@ -37,6 +37,7 @@ import { MeteorCall } from '../../../lib/api/methods' import { getSegmentPartNotes } from '../../../lib/rundownNotifications' import { RankedNote, IMediaObjectIssue } from '../../../lib/api/rundownNotifications' import { Settings } from '../../../lib/Settings' +import { isTranslatableMessage, translateMessage } from '../../../lib/api/TranslatableMessage' export const onRONotificationClick = new ReactiveVar<((e: RONotificationEvent) => void) | undefined>(undefined) export const reloadRundownPlaylistClick = new ReactiveVar<((e: any) => void) | undefined>(undefined) @@ -407,30 +408,29 @@ class RundownViewNotifier extends WithManagedTracker { this.autorun(() => { const newNoteIds: Array = [] const combined = fullNotes.get().concat(localNotes.get()) - combined.forEach((item: TrackedNote & { rank: number }) => { - const id = - item.message + - '-' + - (item.origin.pieceId || item.origin.partId || item.origin.segmentId || item.origin.rundownId) + - '-' + - item.origin.name + - '-' + - item.type + combined.forEach((item: TrackedNote) => { + const { origin, message, type: itemType, rank } = item + const { pieceId, partId, segmentId, rundownId, name, segmentName } = origin + + const translatedMessage = isTranslatableMessage(message) ? translateMessage(message, t) : message + + const notificationId = `${translatedMessage}-${pieceId || partId || segmentId || rundownId}-${name}-${itemType}` + let newNotification = new Notification( - id, - item.type === NoteType.ERROR ? NoticeLevel.CRITICAL : NoticeLevel.WARNING, + notificationId, + itemType === NoteType.ERROR ? NoticeLevel.CRITICAL : NoticeLevel.WARNING, ( <> - {item.origin.name || item.origin.segmentName ? ( + {name || segmentName ? (
- {item.origin.segmentName || item.origin.name} - {item.origin.segmentName && item.origin.name ? `${SEGMENT_DELIMITER}${item.origin.name}` : null} + {segmentName || name} + {segmentName && name ? `${SEGMENT_DELIMITER}${name}` : null}
) : null} -
{item.message || t('There is an unknown problem with the part.')}
+
{translatedMessage || t('There is an unknown problem with the part.')}
), - item.origin.segmentId || 'unknown', + segmentId || 'unknown', getCurrentTime(), true, [ @@ -439,7 +439,7 @@ class RundownViewNotifier extends WithManagedTracker { type: 'default', }, ], - item.rank * 1000 + rank * 1000 ) newNotification.on('action', (notification, type, e) => { switch (type) { @@ -447,15 +447,15 @@ class RundownViewNotifier extends WithManagedTracker { const handler = onRONotificationClick.get() if (handler && typeof handler === 'function') { handler({ - sourceLocator: item.origin, + sourceLocator: origin, }) } } }) - newNoteIds.push(id) + newNoteIds.push(notificationId) - if (!this._notes[id] || !Notification.isEqual(newNotification, this._notes[id])) { - this._notes[id] = newNotification + if (!this._notes[notificationId] || !Notification.isEqual(newNotification, this._notes[notificationId])) { + this._notes[notificationId] = newNotification this._notesDep.changed() } }) @@ -561,6 +561,7 @@ class RundownViewNotifier extends WithManagedTracker { allIssues.forEach((issue) => { const { status, message } = issue + let newNotification: Notification | undefined = undefined if ( status !== RundownAPI.PieceStatusCode.OK && From 4c979e40983edcf019f432343586511e5dfef88a Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Thu, 10 Sep 2020 17:07:01 +0200 Subject: [PATCH 15/58] fix: namespaces not applied when creating notes from blueprint processing --- meteor/client/ui/i18n.ts | 3 +- .../server/api/blueprints/context/context.ts | 57 ++++++++++++++++--- meteor/server/api/blueprints/postProcess.ts | 2 +- meteor/server/api/ingest/bucketAdlibs.ts | 15 +++-- meteor/server/api/ingest/rundownInput.ts | 43 ++++++++++++-- meteor/server/api/playout/playout.ts | 13 +++-- 6 files changed, 108 insertions(+), 25 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 4a22e6612c..d1ae7f2fa3 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -52,7 +52,7 @@ class I18nContainer extends WithManagedTracker { console.error('Error initializing i18Next:', err) } else { this.i18nTranslator = t - console.debug('i18nTranslator init complete') + console.debug(`i18nTranslator init complete, using language ${this.i18nInstance.language}`) } }) @@ -72,7 +72,6 @@ class I18nContainer extends WithManagedTracker { ) console.debug('i18instance updated', { bundle: { lang: bundle.language, ns: bundle.namespace } }) } else { - //TODO: remove, debug use only console.debug(`Skipped bundle, no translations`, { bundle }) } } diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index 516c2866de..8900aea4c2 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -93,7 +93,7 @@ export class NotesContext extends CommonContext implements INotesContext { private readonly _contextName: string private readonly _contextIdentifier: string private _handleNotesExternally: boolean - private _namespaces: Array + private _translationNamespaces: Array private readonly savedNotes: Array = [] @@ -101,7 +101,7 @@ export class NotesContext extends CommonContext implements INotesContext { contextName: string, contextIdentifier: string, handleNotesExternally: boolean, - blueprints?: Array + blueprintIds?: Array ) { super(contextIdentifier) this._contextName = contextName @@ -109,8 +109,9 @@ export class NotesContext extends CommonContext implements INotesContext { /** If the notes will be handled externally (using .getNotes()), set this to true */ this._handleNotesExternally = handleNotesExternally - if (blueprints) { - this._namespaces = blueprints.slice() + if (blueprintIds) { + // when uploaded, translations are bundled using blueprint ids as namespaces + this._translationNamespaces = blueprintIds.slice() } } /** Throw Error and display message to the user in the GUI */ @@ -138,7 +139,7 @@ export class NotesContext extends CommonContext implements INotesContext { if (this._handleNotesExternally) { this.savedNotes.push({ type: type, - message: { key: message, args, namespaces: this._namespaces }, + message: { key: message, args, namespaces: this._translationNamespaces }, trackingId: trackingId, }) } else { @@ -335,13 +336,23 @@ export class RundownContext extends ShowStyleContext implements IRundownContext, readonly playlistId: RundownPlaylistId constructor(rundown: Rundown, cache: CacheForRundownPlaylist, notesContext: NotesContext | undefined) { + const blueprintIds: Set = new Set() + const showStyleBlueprintId = rundown.getShowStyleBase()?.blueprintId + if (showStyleBlueprintId) { + blueprintIds.add(unprotectString(showStyleBlueprintId)) + } + const studioBlueprintId = rundown.getStudio()?.blueprintId + if (studioBlueprintId) { + blueprintIds.add(unprotectString(studioBlueprintId)) + } + super( cache.activationCache.getStudio(), cache, rundown, rundown.showStyleBaseId, rundown.showStyleVariantId, - notesContext || new NotesContext(rundown.name, `rundownId=${rundown._id}`, false) + notesContext || new NotesContext(rundown.name, `rundownId=${rundown._id}`, false, Array.from(blueprintIds)) ) this.rundownId = unprotectString(rundown._id) @@ -375,10 +386,25 @@ export class PartEventContext extends RundownContext implements IPartEventContex readonly part: Readonly constructor(rundown: Rundown, cache: CacheForRundownPlaylist, partInstance: PartInstance) { + const blueprintIds: Set = new Set() + const showStyleBlueprintId = rundown.getShowStyleBase()?.blueprintId + if (showStyleBlueprintId) { + blueprintIds.add(unprotectString(showStyleBlueprintId)) + } + const studioBlueprintId = rundown.getStudio()?.blueprintId + if (studioBlueprintId) { + blueprintIds.add(unprotectString(studioBlueprintId)) + } + super( rundown, cache, - new NotesContext(rundown.name, `rundownId=${rundown._id},partInstanceId=${partInstance._id}`, false) + new NotesContext( + rundown.name, + `rundownId=${rundown._id},partInstanceId=${partInstance._id}`, + false, + Array.from(blueprintIds) + ) ) this.part = unprotectPartInstance(partInstance) @@ -393,10 +419,25 @@ export class AsRunEventContext extends RundownContext implements IAsRunEventCont public readonly asRunEvent: Readonly constructor(rundown: Rundown, cache: CacheForRundownPlaylist, asRunEvent: AsRunLogEvent) { + const blueprintIds: Set = new Set() + const showStyleBlueprintId = rundown.getShowStyleBase()?.blueprintId + if (showStyleBlueprintId) { + blueprintIds.add(unprotectString(showStyleBlueprintId)) + } + const studioBlueprintId = rundown.getStudio()?.blueprintId + if (studioBlueprintId) { + blueprintIds.add(unprotectString(studioBlueprintId)) + } + super( rundown, cache, - new NotesContext(rundown.name, `rundownId=${rundown._id},asRunEventId=${asRunEvent._id}`, false) + new NotesContext( + rundown.name, + `rundownId=${rundown._id},asRunEventId=${asRunEvent._id}`, + false, + Array.from(blueprintIds) + ) ) this.asRunEvent = unprotectObject(asRunEvent) } diff --git a/meteor/server/api/blueprints/postProcess.ts b/meteor/server/api/blueprints/postProcess.ts index 95a415bd1e..598a5e8fc2 100644 --- a/meteor/server/api/blueprints/postProcess.ts +++ b/meteor/server/api/blueprints/postProcess.ts @@ -217,7 +217,7 @@ export function postProcessStudioBaselineObjects(studio: Studio, objs: TSR.TSRTi 'studio', 'studio', false, - studio.blueprintId ? [unprotectString(studio.blueprintId)] : undefined + studio.blueprintId ? [unprotectString(studio.blueprintId)] : [] ) return postProcessTimelineObjects( context, diff --git a/meteor/server/api/ingest/bucketAdlibs.ts b/meteor/server/api/ingest/bucketAdlibs.ts index db5da36d9e..82cfbd55c4 100644 --- a/meteor/server/api/ingest/bucketAdlibs.ts +++ b/meteor/server/api/ingest/bucketAdlibs.ts @@ -25,18 +25,21 @@ export function updateBucketAdlibFromIngestData( ): PieceId | null { const { blueprint, blueprintId } = loadShowStyleBlueprint(showStyle) + const blueprintIds: Set = new Set() + if (blueprintId) { + blueprintIds.add(unprotectString(blueprintId)) + } + if (studio.blueprintId) { + blueprintIds.add(unprotectString(studio.blueprintId)) + } + const context = new ShowStyleContext( studio, undefined, undefined, showStyle._id, showStyle.showStyleVariantId, - new NotesContext( - 'Bucket Ad-Lib', - 'bucket-adlib', - false, - [blueprintId, studio.blueprintId].map(unprotectString).filter((id): id is string => id !== undefined) - ) + new NotesContext('Bucket Ad-Lib', 'bucket-adlib', false, Array.from(blueprintIds)) ) if (!blueprint.getAdlibItem) throw new Meteor.Error(501, "This blueprint doesn't support ingest AdLibs") const rawAdlib = blueprint.getAdlibItem(context, ingestData) diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index 48d91a4784..393ecbe7f2 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -450,10 +450,20 @@ function updateRundownFromIngestData( } const showStyleBlueprint = loadShowStyleBlueprint(showStyle.base).blueprint + + const blueprintIds: Set = new Set() + if (showStyleBlueprint.blueprintId) { + blueprintIds.add(showStyleBlueprint.blueprintId) + } + if (studio.blueprintId) { + blueprintIds.add(unprotectString(studio.blueprintId)) + } + const notesContext = new NotesContext( `${showStyle.base.name}-${showStyle.variant.name}`, `showStyleBaseId=${showStyle.base._id},showStyleVariantId=${showStyle.variant._id}`, - true + true, + Array.from(blueprintIds) ) const blueprintContext = new ShowStyleContext( studio, @@ -619,7 +629,13 @@ function updateRundownFromIngestData( const cache = waitForPromise(initCacheForRundownPlaylist(dbPlaylist)) // Save the baseline - const rundownNotesContext = new NotesContext(dbRundown.name, `rundownId=${dbRundown._id}`, true) + + const rundownNotesContext = new NotesContext( + dbRundown.name, + `rundownId=${dbRundown._id}`, + true, + Array.from(blueprintIds) + ) const blueprintRundownContext = new RundownContext(dbRundown, cache, rundownNotesContext) logger.info(`Building baseline objects for ${dbRundown._id}...`) logger.info(`... got ${rundownRes.baseline.length} objects from baseline.`) @@ -660,6 +676,7 @@ function updateRundownFromIngestData( const adlibActions: AdLibAction[] = [] const { blueprint, blueprintId } = loadShowStyleBlueprint(showStyle.base) + blueprintIds.add(unprotectString(blueprintId)) _.each(ingestRundown.segments, (ingestSegment: IngestSegment) => { const segmentId = getSegmentId(rundownId, ingestSegment.externalId) @@ -668,7 +685,12 @@ function updateRundownFromIngestData( ingestSegment.parts = _.sortBy(ingestSegment.parts, (part) => part.rank) - const notesContext = new NotesContext(ingestSegment.name, `rundownId=${rundownId},segmentId=${segmentId}`, true) + const notesContext = new NotesContext( + ingestSegment.name, + `rundownId=${rundownId},segmentId=${segmentId}`, + true, + Array.from(blueprintIds) + ) const context = new SegmentContext(dbRundown, cache, notesContext) const res = blueprint.getSegment(context, ingestSegment) @@ -1108,7 +1130,20 @@ function updateSegmentFromIngestData( ingestSegment.parts = _.sortBy(ingestSegment.parts, (s) => s.rank) - const notesContext = new NotesContext(ingestSegment.name, `rundownId=${rundown._id},segmentId=${segmentId}`, true) + const blueprintIds: Set = new Set() + if (blueprintId) { + blueprintIds.add(unprotectString(blueprintId)) + } + if (studio.blueprintId) { + blueprintIds.add(unprotectString(studio.blueprintId)) + } + + const notesContext = new NotesContext( + ingestSegment.name, + `rundownId=${rundown._id},segmentId=${segmentId}`, + true, + Array.from(blueprintIds) + ) const context = new SegmentContext(rundown, cache, notesContext) const res = blueprint.getSegment(context, ingestSegment) diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index 3c829143d8..13e64a6d6f 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -1098,16 +1098,21 @@ export namespace ServerPlayoutAPI { if (!rundown) throw new Meteor.Error(501, `Current Rundown "${currentPartInstance.rundownId}" could not be found`) - const usedBlueprints = [studio.blueprintId, rundown.getShowStyleBase().blueprintId] - .map(unprotectString) - .filter((id): id is string => id !== undefined) + const blueprintIds: Set = new Set() + if (studio.blueprintId) { + blueprintIds.add(unprotectString(studio.blueprintId)) + } + if (rundown.getShowStyleBase()?.blueprintId) { + blueprintIds.add(unprotectString(rundown.getShowStyleBase().blueprintId)) + } + const notesContext = new NotesContext( `${rundown.name}(${playlist.name})`, `playlist=${playlist._id},rundown=${rundown._id},currentPartInstance=${ currentPartInstance._id },execution=${getRandomId()}`, false, - usedBlueprints + Array.from(blueprintIds) ) const actionContext = new ActionExecutionContext(cache, notesContext, studio, playlist, rundown) From d992aa7e0a74b6149867c8d5c3f57f98d10a3711 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 21 Sep 2020 17:44:52 +0100 Subject: [PATCH 16/58] chore: update blueprints-integration --- meteor/package-lock.json | 14 +++++++------- meteor/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/meteor/package-lock.json b/meteor/package-lock.json index 990138c6a1..553a3ebcd5 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17178,9 +17178,9 @@ "integrity": "sha512-xFVZlWnqls5eVphtUJo0UuXeJa0grBmxESSn5QPQB7CMsaVgTnLdP9PTZqb4KyFmLdrE+PUu3eEt42zHsMxCtw==" }, "timeline-state-resolver-types": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/timeline-state-resolver-types/-/timeline-state-resolver-types-4.0.3.tgz", - "integrity": "sha512-urKK2rTz2FKpPUxARJoVAorQuKdFHV45lXxQmNJd62SVDXHqxN0djv5YKj4zgX4qN/7IrEmQKCPNEWt2iPCzAA==", + "version": "5.0.0-nightly-release24-20200914-120931-3357b9dc.0", + "resolved": "https://registry.npmjs.org/timeline-state-resolver-types/-/timeline-state-resolver-types-5.0.0-nightly-release24-20200914-120931-3357b9dc.0.tgz", + "integrity": "sha512-gOGcK+d2D0diBs2kLlOEJ3VyWN5eS42jwboGRlaMMvLLpAYs0uZ/I1Uv2dtxJAreQxFEACcGK3GXoOO6TkZuxw==", "requires": { "tslib": "^1.13.0" } @@ -17474,12 +17474,12 @@ } }, "tv-automation-sofie-blueprints-integration": { - "version": "2.2.0-nightly-release25-20200915-143503-e8ed9664.0", - "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-2.2.0-nightly-release25-20200915-143503-e8ed9664.0.tgz", - "integrity": "sha512-Ew2EGg68VFmHBnBwJ9XY2RmhfM9S+HW5SnDaVDr2siUdhamrtUPwDuzDRy0a3n5Dn/oQBcOtUXiu7Rrn0ICEpQ==", + "version": "3.0.0-nightly-feat-restructure-logging-20200921-162726-65f11616.0", + "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-restructure-logging-20200921-162726-65f11616.0.tgz", + "integrity": "sha512-8uUVYQgEM0P9OXmCLAc8GACBRD8nd8cIPbTaYy3utOSdspyn44oOIHxnotD6tLPSn7isHGoiXuOlfvo0tUitfw==", "requires": { "moment": "2.22.2", - "timeline-state-resolver-types": "^4.0.0", + "timeline-state-resolver-types": "5.0.0-nightly-release24-20200914-120931-3357b9dc.0", "tslib": "^1.13.0", "underscore": "1.9.1" }, diff --git a/meteor/package.json b/meteor/package.json index e3032d1330..8b28afc665 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -96,7 +96,7 @@ "soap": "^0.31.0", "superfly-timeline": "^7.3.1", "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "2.2.0-nightly-release25-20200915-143503-e8ed9664.0", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-restructure-logging-20200921-162726-65f11616.0", "underscore": "^1.11.0", "utility-types": "^3.10.0", "velocity-animate": "^1.5.2", From 469156680d3b0c3ccf410a4380219d90ebd888e4 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 25 Sep 2020 14:11:31 +0100 Subject: [PATCH 17/58] chore: update blueprints-integration --- meteor/package-lock.json | 6 +++--- meteor/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meteor/package-lock.json b/meteor/package-lock.json index 553a3ebcd5..9331e2b24b 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17474,9 +17474,9 @@ } }, "tv-automation-sofie-blueprints-integration": { - "version": "3.0.0-nightly-feat-restructure-logging-20200921-162726-65f11616.0", - "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-restructure-logging-20200921-162726-65f11616.0.tgz", - "integrity": "sha512-8uUVYQgEM0P9OXmCLAc8GACBRD8nd8cIPbTaYy3utOSdspyn44oOIHxnotD6tLPSn7isHGoiXuOlfvo0tUitfw==", + "version": "3.0.0-nightly-feat-localisation-20200921-162712-68edb847.0", + "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200921-162712-68edb847.0.tgz", + "integrity": "sha512-tdL7XBmrjBZ/03EyX8LmEzCsBg4n2Q415BtWH2hgXTeUBCmh5pdGsQk+ed8WEiljG1Abp/8pgZPEwzHZyMV6/w==", "requires": { "moment": "2.22.2", "timeline-state-resolver-types": "5.0.0-nightly-release24-20200914-120931-3357b9dc.0", diff --git a/meteor/package.json b/meteor/package.json index 8b28afc665..d0f2fdd4be 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -96,7 +96,7 @@ "soap": "^0.31.0", "superfly-timeline": "^7.3.1", "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-restructure-logging-20200921-162726-65f11616.0", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200921-162712-68edb847.0", "underscore": "^1.11.0", "utility-types": "^3.10.0", "velocity-animate": "^1.5.2", From b419f4afa0cf017abb32ed87e1c555cad500d63c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 25 Sep 2020 14:22:25 +0100 Subject: [PATCH 18/58] chore: update blueprints-integration --- meteor/package-lock.json | 6 +++--- meteor/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meteor/package-lock.json b/meteor/package-lock.json index 7d5631c727..b1b06b55a9 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17462,9 +17462,9 @@ } }, "tv-automation-sofie-blueprints-integration": { - "version": "3.0.0-nightly-feat-localisation-20200921-162712-68edb847.0", - "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200921-162712-68edb847.0.tgz", - "integrity": "sha512-tdL7XBmrjBZ/03EyX8LmEzCsBg4n2Q415BtWH2hgXTeUBCmh5pdGsQk+ed8WEiljG1Abp/8pgZPEwzHZyMV6/w==", + "version": "3.0.0-nightly-feat-localisation-20200925-131650-a7a0fd91.0", + "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200925-131650-a7a0fd91.0.tgz", + "integrity": "sha512-EtjPu0dY7C0CkngNN+Y1Pp9MHtO5CIAAg9zkMW84u46p7Mul27s6t6y8yyGIiYPH805AUADCsxDjK6/Vu4W0gA==", "requires": { "moment": "2.22.2", "timeline-state-resolver-types": "5.0.0-nightly-release24-20200914-120931-3357b9dc.0", diff --git a/meteor/package.json b/meteor/package.json index de6360b94d..3d9f7fa504 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -95,7 +95,7 @@ "soap": "^0.31.0", "superfly-timeline": "^8.1.1", "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200921-162712-68edb847.0", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200925-131650-a7a0fd91.0", "underscore": "^1.11.0", "utility-types": "^3.10.0", "velocity-animate": "^1.5.2", From 50c4a0b2be53646427ba55191c24ae6727a05bb7 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 21 Sep 2020 16:21:14 +0100 Subject: [PATCH 19/58] feat: blueprint logging api changes --- meteor/server/api/asRunLog.ts | 10 +- meteor/server/api/blueprints/config.ts | 13 +- .../api/blueprints/context/adlibActions.ts | 39 +- .../server/api/blueprints/context/context.ts | 388 ++++++++++-------- meteor/server/api/blueprints/postProcess.ts | 59 ++- meteor/server/api/ingest/bucketAdlibs.ts | 30 +- meteor/server/api/ingest/rundownInput.ts | 181 ++++---- meteor/server/api/playout/actions.ts | 16 +- meteor/server/api/playout/playout.ts | 37 +- meteor/server/api/playout/take.ts | 33 +- meteor/server/api/playout/timeline.ts | 23 +- meteor/server/api/rundown.ts | 15 +- 12 files changed, 503 insertions(+), 341 deletions(-) diff --git a/meteor/server/api/asRunLog.ts b/meteor/server/api/asRunLog.ts index fa3da8e690..0264f98e8c 100644 --- a/meteor/server/api/asRunLog.ts +++ b/meteor/server/api/asRunLog.ts @@ -92,7 +92,15 @@ function handleAsRunEvent(event: AsRunLogEvent): void { if (blueprint.onAsRunEvent) { const cache = waitForPromise(initReadOnlyCacheForRundownPlaylist(playlist)) - const context = new AsRunEventContext(rundown, cache, event) + const context = new AsRunEventContext( + { + name: rundown.name, + identifier: `rundownId=${rundown._id},eventId=${event._id}`, + }, + rundown, + cache, + event + ) Promise.resolve(blueprint.onAsRunEvent(context)) .then((messages: Array) => { diff --git a/meteor/server/api/blueprints/config.ts b/meteor/server/api/blueprints/config.ts index f4eccb0210..c8292aec73 100644 --- a/meteor/server/api/blueprints/config.ts +++ b/meteor/server/api/blueprints/config.ts @@ -13,6 +13,7 @@ import { Meteor } from 'meteor/meteor' import { getShowStyleCompound, ShowStyleVariantId, ShowStyleCompound } from '../../../lib/collections/ShowStyleVariants' import { protectString, objectPathGet, objectPathSet } from '../../../lib/lib' import { logger } from '../../../lib/logging' +import { CommonContext } from './context' /** * This whole ConfigRef logic will need revisiting for a multi-studio context, to ensure that there are strict boundaries across who can give to access to what. @@ -85,7 +86,11 @@ export function preprocessStudioConfig(studio: Studio, blueprint?: StudioBluepri res['SofieHostURL'] = studio.settings.sofieUrl if (blueprint && blueprint.preprocessConfig) { - res = blueprint.preprocessConfig(res) + const context = new CommonContext({ + name: `preprocessStudioConfig`, + identifier: `studioId=${studio._id}`, + }) + res = blueprint.preprocessConfig(context, res) } return res } @@ -98,7 +103,11 @@ export function preprocessShowStyleConfig(showStyle: ShowStyleCompound, blueprin res = showStyle.blueprintConfig } if (blueprint && blueprint.preprocessConfig) { - res = blueprint.preprocessConfig(res) + const context = new CommonContext({ + name: `preprocessShowStyleConfig`, + identifier: `showStyleBaseId=${showStyle._id},showStyleVariantId=${showStyle.showStyleVariantId}`, + }) + res = blueprint.preprocessConfig(context, res) } return res } diff --git a/meteor/server/api/blueprints/context/adlibActions.ts b/meteor/server/api/blueprints/context/adlibActions.ts index 1037aa0c57..95077450e5 100644 --- a/meteor/server/api/blueprints/context/adlibActions.ts +++ b/meteor/server/api/blueprints/context/adlibActions.ts @@ -33,11 +33,12 @@ import { PartInstanceId, PartInstance } from '../../../../lib/collections/PartIn import { CacheForRundownPlaylist } from '../../../DatabaseCaches' import { getResolvedPieces } from '../../playout/pieces' import { postProcessPieces, postProcessTimelineObjects } from '../postProcess' -import { NotesContext, ShowStyleContext, EventContext } from './context' +import { ShowStyleContext, ContextInfo, UserContextInfo, RawNote } from './context' import { isTooCloseToAutonext } from '../../playout/lib' import { ServerPlayoutAdLibAPI } from '../../playout/adlib' import { MongoQuery } from '../../../../lib/typings/meteor' import { clone } from '../../../../lib/lib' +import { NoteType } from '../../../../lib/api/notes' export enum ActionPartChange { NONE = 0, @@ -99,6 +100,9 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE private readonly rundownPlaylist: RundownPlaylist private readonly rundown: Rundown + public readonly notes: RawNote[] = [] + private readonly blackHoleNotes: boolean + private queuedPartInstance: PartInstance | undefined /** To be set by any mutation methods on this context. Indicates to core how extensive the changes are to the current partInstance */ @@ -108,19 +112,48 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE public takeAfterExecute: boolean constructor( + contextInfo: UserContextInfo, cache: CacheForRundownPlaylist, - notesContext: NotesContext, studio: Studio, rundownPlaylist: RundownPlaylist, rundown: Rundown ) { - super(studio, cache, rundown, rundown.showStyleBaseId, rundown.showStyleVariantId, notesContext) + super(contextInfo, studio, cache, rundown, rundown.showStyleBaseId, rundown.showStyleVariantId) this._cache = cache this.rundownPlaylist = rundownPlaylist this.rundown = rundown this.takeAfterExecute = false } + userError(message: string, params?: { [key: string]: any }, trackingId?: string): void { + if (this.blackHoleNotes) { + this.logError(message) + } else { + this.notes.push({ + type: NoteType.ERROR, + message: { + key: message, + args: params, + }, + trackingId: trackingId, + }) + } + } + userWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { + if (this.blackHoleNotes) { + this.logWarning(message) + } else { + this.notes.push({ + type: NoteType.WARNING, + message: { + key: message, + args: params, + }, + trackingId: trackingId, + }) + } + } + private _getPartInstanceId(part: 'current' | 'next'): PartInstanceId | null { switch (part) { case 'current': diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index b55776a92e..b1e6ebc6c9 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -18,16 +18,16 @@ import { check, Match } from '../../../../lib/check' import { logger } from '../../../../lib/logging' import { ICommonContext, - NotesContext as INotesContext, + IUserNotesContext, ShowStyleContext as IShowStyleContext, RundownContext as IRundownContext, - SegmentContext as ISegmentContext, + SegmentUserContext as ISegmentUserContext, EventContext as IEventContext, AsRunEventContext as IAsRunEventContext, PartEventContext as IPartEventContext, TimelineEventContext as ITimelineEventContext, - IStudioConfigContext, - IStudioContext, + StudioContext as IStudioContext, + StudioUserContext as IStudioUserContext, BlueprintMappings, IBlueprintSegmentDB, IngestPart, @@ -38,6 +38,7 @@ import { IBlueprintAsRunLogEvent, IBlueprintExternalMessageQueueObj, ExtendedIngestRundown, + ShowStyleBlueprintManifest, } from 'tv-automation-sofie-blueprints-integration' import { Studio, StudioId, Studios } from '../../../../lib/collections/Studios' import { ConfigRef, preprocessStudioConfig, findMissingConfigs, preprocessShowStyleConfig } from '../config' @@ -59,17 +60,25 @@ import { ExternalMessageQueue } from '../../../../lib/collections/ExternalMessag import { extendIngestRundownCore } from '../../ingest/lib' import { loadStudioBlueprint, loadShowStyleBlueprint } from '../cache' import { CacheForRundownPlaylist, ReadOnlyCacheForRundownPlaylist } from '../../../DatabaseCaches' -import { getSelectedPartInstancesFromCache } from '../../playout/lib' +import { BlueprintId } from '../../../../lib/collections/Blueprints' + +export interface ContextInfo { + name: string + identifier: string +} +export interface UserContextInfo extends ContextInfo { + blackHoleUserNotes?: boolean // TODO-CONTEXT remove this +} /** Common */ export class CommonContext implements ICommonContext { - private _idPrefix: string = '' + private readonly _contextIdentifier: string private hashI = 0 private hashed: { [hash: string]: string } = {} - constructor(idPrefix: string) { - this._idPrefix = idPrefix + constructor(info: ContextInfo) { + this._contextIdentifier = info.identifier } getHashId(str: string, isNotUnique?: boolean) { if (!str) str = 'hash' + this.hashI++ @@ -78,90 +87,36 @@ export class CommonContext implements ICommonContext { str = str + '_' + this.hashI++ } - const id = getHash(this._idPrefix + '_' + str.toString()) + const id = getHash(this._contextIdentifier + '_' + str.toString()) this.hashed[id] = str return id } unhashId(hash: string): string { return this.hashed[hash] || hash } -} - -export interface RawNote extends INoteBase { - trackingId: string | undefined -} - -export class NotesContext extends CommonContext implements INotesContext { - private readonly _contextName: string - private readonly _contextIdentifier: string - private _handleNotesExternally: boolean - private _translationNamespaces: Array - - private readonly savedNotes: Array = [] - constructor( - contextName: string, - contextIdentifier: string, - handleNotesExternally: boolean, - blueprintIds?: Array - ) { - super(contextIdentifier) - this._contextName = contextName - this._contextIdentifier = contextIdentifier - /** If the notes will be handled externally (using .getNotes()), set this to true */ - this._handleNotesExternally = handleNotesExternally - - if (blueprintIds) { - // when uploaded, translations are bundled using blueprint ids as namespaces - this._translationNamespaces = blueprintIds.slice() - } - } - /** Throw Error and display message to the user in the GUI */ - error(message: string, params?: { [key: string]: any }, trackingId?: string) { - check(message, String) - logger.error('Error from blueprint: ' + message) - this._pushNote(NoteType.ERROR, message, params, trackingId) - throw new Meteor.Error(500, message) + logDebug(message: string): void { + // TODO - prefix with _contextIdentifier? + logger.debug(message) } - /** Save note, which will be displayed to the user in the GUI */ - warning(message: string, params?: { [key: string]: any }, trackingId?: string) { - check(message, String) - this._pushNote(NoteType.WARNING, message, params, trackingId) + logInfo(message: string): void { + // TODO - prefix with _contextIdentifier? + logger.info(message) } - getNotes(): RawNote[] { - return this.savedNotes + logWarning(message: string): void { + // TODO - prefix with _contextIdentifier? + logger.warn(message) } - get handleNotesExternally(): boolean { - return this._handleNotesExternally - } - set handleNotesExternally(value: boolean) { - this._handleNotesExternally = value - } - protected _pushNote(type: NoteType, message: string, args?: { [key: string]: any }, trackingId?: string) { - if (this._handleNotesExternally) { - this.savedNotes.push({ - type: type, - message: { key: message, args, namespaces: this._translationNamespaces }, - trackingId: trackingId, - }) - } else { - if (type === NoteType.WARNING) { - logger.warn( - `Warning from "${this._contextName}"${trackingId ? `(${trackingId})` : ''}: "${message}"\n(${ - this._contextIdentifier - })` - ) - } else { - logger.error( - `Error from "${this._contextName}"${trackingId ? `(${trackingId})` : ''}: "${message}"\n(${ - this._contextIdentifier - })` - ) - } - } + logError(message: string): void { + // TODO - prefix with _contextIdentifier? + logger.error(message) } } +export interface RawNote extends INoteBase { + trackingId: string | undefined +} + const studioBlueprintConfigCache: { [studioId: string]: Cache } = {} const showStyleBlueprintConfigCache: { [showStyleBaseId: string]: { [showStyleVariantId: string]: Cache } } = {} interface Cache { @@ -170,9 +125,10 @@ interface Cache { /** Studio */ -export class StudioConfigContext implements IStudioConfigContext { +export class StudioContext extends CommonContext implements IStudioContext { protected readonly studio: Studio - constructor(studio: Studio) { + constructor(contextInfo: ContextInfo, studio: Studio) { + super(contextInfo) this.studio = studio } @@ -216,30 +172,61 @@ export class StudioConfigContext implements IStudioConfigContext { getStudioConfigRef(configKey: string): string { return ConfigRef.getStudioConfigRef(this.studio._id, configKey) } -} -export class StudioContext extends StudioConfigContext implements IStudioContext { getStudioMappings(): Readonly { return this.studio.mappings } } +export class StudioUserContext extends StudioContext implements IStudioUserContext { + public readonly notes: INoteBase[] = [] + private readonly blackHoleNotes: boolean + + constructor(contextInfo: UserContextInfo, studio: Studio) { + super(contextInfo, studio) + this.blackHoleNotes = contextInfo.blackHoleUserNotes ?? false + } + + userError(message: string, params?: { [key: string]: any }): void { + if (this.blackHoleNotes) { + this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.ERROR, + message: { + key: message, + args: params, + }, + }) + } + } + userWarning(message: string, params?: { [key: string]: any }): void { + if (this.blackHoleNotes) { + this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.WARNING, + message: { + key: message, + args: params, + }, + }) + } + } +} + /** Show Style Variant */ export class ShowStyleContext extends StudioContext implements IShowStyleContext { - readonly notesContext: NotesContext - constructor( + contextInfo: ContextInfo, studio: Studio, private readonly cache: ReadOnlyCacheForRundownPlaylist | undefined, readonly _rundown: Rundown | undefined, readonly showStyleBaseId: ShowStyleBaseId, - readonly showStyleVariantId: ShowStyleVariantId, - notesContext: NotesContext + readonly showStyleVariantId: ShowStyleVariantId ) { - super(studio) - - this.notesContext = notesContext + super(contextInfo, studio) } getShowStyleBase(): ShowStyleBase { @@ -307,54 +294,67 @@ export class ShowStyleContext extends StudioContext implements IShowStyleContext getShowStyleConfigRef(configKey: string): string { return ConfigRef.getShowStyleConfigRef(this.showStyleVariantId, configKey) } +} - /** NotesContext */ - error(message: string, params?: { [key: string]: any }, trackingId?: string) { - this.notesContext.error(message, params, trackingId) - } - warning(message: string, params?: { [key: string]: any }, trackingId?: string) { - this.notesContext.warning(message, params, trackingId) - } - getHashId(str: string, isNotUnique?: boolean) { - return this.notesContext.getHashId(str, isNotUnique) - } - unhashId(hash: string) { - return this.notesContext.unhashId(hash) +export class ShowStyleUserContext extends ShowStyleContext implements IUserNotesContext { + public readonly notes: INoteBase[] = [] + private readonly blackHoleNotes: boolean + + constructor( + contextInfo: UserContextInfo, + studio: Studio, + cache: CacheForRundownPlaylist | undefined, + _rundown: Rundown | undefined, + showStyleBaseId: ShowStyleBaseId, + showStyleVariantId: ShowStyleVariantId + ) { + super(contextInfo, studio, cache, _rundown, showStyleBaseId, showStyleVariantId) } - get handleNotesExternally(): boolean { - return this.notesContext.handleNotesExternally + + userError(message: string, params?: { [key: string]: any }): void { + if (this.blackHoleNotes) { + this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.ERROR, + message: { + key: message, + args: params, + }, + }) + } } - set handleNotesExternally(value: boolean) { - this.notesContext.handleNotesExternally = value + userWarning(message: string, params?: { [key: string]: any }): void { + if (this.blackHoleNotes) { + this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.WARNING, + message: { + key: message, + args: params, + }, + }) + } } } /** Rundown */ -export class RundownContext extends ShowStyleContext implements IRundownContext, IEventContext { +export class RundownContext extends ShowStyleContext implements IRundownContext { readonly rundownId: string readonly rundown: Readonly readonly _rundown: Rundown readonly playlistId: RundownPlaylistId - constructor(rundown: Rundown, cache: ReadOnlyCacheForRundownPlaylist, notesContext: NotesContext | undefined) { - const blueprintIds: Set = new Set() - const showStyleBlueprintId = rundown.getShowStyleBase()?.blueprintId - if (showStyleBlueprintId) { - blueprintIds.add(unprotectString(showStyleBlueprintId)) - } - const studioBlueprintId = rundown.getStudio()?.blueprintId - if (studioBlueprintId) { - blueprintIds.add(unprotectString(studioBlueprintId)) - } - + constructor(contextInfo: ContextInfo, rundown: Rundown, cache: ReadOnlyCacheForRundownPlaylist) { super( + contextInfo, cache.activationCache.getStudio(), cache, rundown, rundown.showStyleBaseId, - rundown.showStyleVariantId, - notesContext || new NotesContext(rundown.name, `rundownId=${rundown._id}`, false, Array.from(blueprintIds)) + rundown.showStyleVariantId ) this.rundownId = unprotectString(rundown._id) @@ -362,15 +362,51 @@ export class RundownContext extends ShowStyleContext implements IRundownContext, this._rundown = rundown this.playlistId = rundown.playlistId } +} + +export class RundownEventContext extends RundownContext implements IEventContext { + constructor(blueprintId: BlueprintId, rundown: Rundown, cache: CacheForRundownPlaylist) { + super( + { + name: rundown.name, + identifier: `rundownId=${rundown._id},blueprintId=${blueprintId}`, + }, + rundown, + cache + ) + } getCurrentTime(): number { return getCurrentTime() } } -export class SegmentContext extends RundownContext implements ISegmentContext { - constructor(rundown: Rundown, cache: CacheForRundownPlaylist, notesContext: NotesContext) { - super(rundown, cache, notesContext) +export class SegmentUserContext extends RundownContext implements ISegmentUserContext { + public readonly notes: RawNote[] = [] + + constructor(contextInfo: ContextInfo, rundown: Rundown, cache: CacheForRundownPlaylist) { + super(contextInfo, rundown, cache) + } + + userError(message: string, params?: { [key: string]: any }, trackingId?: string): void { + this.notes.push({ + type: NoteType.ERROR, + message: { + key: message, + args: params, + }, + trackingId: trackingId, + }) + } + userWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { + this.notes.push({ + type: NoteType.WARNING, + message: { + key: message, + args: params, + }, + trackingId: trackingId, + }) } } @@ -387,26 +423,20 @@ export class EventContext extends CommonContext implements IEventContext { export class PartEventContext extends RundownContext implements IPartEventContext { readonly part: Readonly - constructor(rundown: Rundown, cache: CacheForRundownPlaylist, partInstance: PartInstance) { - const blueprintIds: Set = new Set() - const showStyleBlueprintId = rundown.getShowStyleBase()?.blueprintId - if (showStyleBlueprintId) { - blueprintIds.add(unprotectString(showStyleBlueprintId)) - } - const studioBlueprintId = rundown.getStudio()?.blueprintId - if (studioBlueprintId) { - blueprintIds.add(unprotectString(studioBlueprintId)) - } - + constructor( + eventName: string, + blueprintId: BlueprintId, + rundown: Rundown, + cache: CacheForRundownPlaylist, + partInstance: PartInstance + ) { super( + { + name: `Event: ${eventName}`, + identifier: `rundownId=${rundown._id},blueprintId=${blueprintId}`, + }, rundown, - cache, - new NotesContext( - rundown.name, - `rundownId=${rundown._id},partInstanceId=${partInstance._id}`, - false, - Array.from(blueprintIds) - ) + cache ) this.part = unprotectPartInstance(partInstance) @@ -421,58 +451,80 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve readonly currentPartInstance: Readonly | undefined readonly nextPartInstance: Readonly | undefined + public readonly notes: RawNote[] = [] + private readonly blackHoleNotes: boolean + constructor( + contextInfo: UserContextInfo, rundown: Rundown, cache: CacheForRundownPlaylist, currentPartInstance: PartInstance | undefined, nextPartInstance: PartInstance | undefined ) { - super( - rundown, - cache, - new NotesContext( - rundown.name, - `rundownId=${rundown._id},currentPartInstance=${currentPartInstance?._id},nextPartInstance=${nextPartInstance?._id}`, - false - ) - ) + super(contextInfo, rundown, cache) this.currentPartInstance = currentPartInstance ? unprotectPartInstance(currentPartInstance) : undefined this.nextPartInstance = nextPartInstance ? unprotectPartInstance(nextPartInstance) : undefined + + this.blackHoleNotes = contextInfo.blackHoleUserNotes ?? false } getCurrentTime(): number { return getCurrentTime() } + + userError(message: string, params?: { [key: string]: any }, trackingId?: string): void { + if (this.blackHoleNotes) { + this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.ERROR, + message: { + key: message, + args: params, + }, + trackingId: trackingId, + }) + } + } + userWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { + if (this.blackHoleNotes) { + this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.WARNING, + message: { + key: message, + args: params, + }, + trackingId: trackingId, + }) + } + } } export class AsRunEventContext extends RundownContext implements IAsRunEventContext { public readonly asRunEvent: Readonly - constructor(rundown: Rundown, cache: ReadOnlyCacheForRundownPlaylist, asRunEvent: AsRunLogEvent) { - const blueprintIds: Set = new Set() - const showStyleBlueprintId = rundown.getShowStyleBase()?.blueprintId - if (showStyleBlueprintId) { - blueprintIds.add(unprotectString(showStyleBlueprintId)) - } - const studioBlueprintId = rundown.getStudio()?.blueprintId - if (studioBlueprintId) { - blueprintIds.add(unprotectString(studioBlueprintId)) - } - + constructor( + contextInfo: ContextInfo, + rundown: Rundown, + cache: ReadOnlyCacheForRundownPlaylist, + asRunEvent: AsRunLogEvent + ) { super( + contextInfo, rundown, - cache, - new NotesContext( - rundown.name, - `rundownId=${rundown._id},asRunEventId=${asRunEvent._id}`, - false, - Array.from(blueprintIds) - ) + cache + // new NotesContext(rundown.name, `rundownId=${rundown._id},asRunEventId=${asRunEvent._id}`, false) ) this.asRunEvent = unprotectObject(asRunEvent) } + getCurrentTime(): number { + return getCurrentTime() + } + /** Get all asRunEvents in the rundown */ getAllAsRunEvents(): Array { return unprotectObjectArray( diff --git a/meteor/server/api/blueprints/postProcess.ts b/meteor/server/api/blueprints/postProcess.ts index 7d55b3a74d..47ce4f2e6d 100644 --- a/meteor/server/api/blueprints/postProcess.ts +++ b/meteor/server/api/blueprints/postProcess.ts @@ -13,15 +13,13 @@ import { TimelineObjectCoreExt, IBlueprintPiece, IBlueprintAdLibPiece, - RundownContext, TSR, IBlueprintActionManifest, - NotesContext as INotesContext, + ICommonContext, } from 'tv-automation-sofie-blueprints-integration' import { RundownAPI } from '../../../lib/api/rundown' import { BucketAdLib } from '../../../lib/collections/BucketAdlibs' -import { ShowStyleContext, NotesContext } from './context' -import { RundownImportVersions } from '../../../lib/collections/Rundowns' +import { RundownImportVersions, Rundown } from '../../../lib/collections/Rundowns' import { BlueprintId } from '../../../lib/collections/Blueprints' import { PartId } from '../../../lib/collections/Parts' import { BucketId } from '../../../lib/collections/Buckets' @@ -31,6 +29,7 @@ import { RundownId } from '../../../lib/collections/Rundowns' import { prefixAllObjectIds } from '../playout/lib' import { SegmentId } from '../../../lib/collections/Segments' import { profiler } from '../profiler' +import { CommonContext, StudioContext, ShowStyleContext } from './context' export function postProcessPieces( innerContext: ShowStyleContext, @@ -101,7 +100,7 @@ function isNow(enable: TSR.TSRTimelineObjBase['enable']): boolean { } export function postProcessTimelineObjects( - innerContext: INotesContext, + innerContext: ICommonContext, pieceId: PieceId, blueprintId: BlueprintId, timelineObjects: TSR.TSRTimelineObjBase[], @@ -144,10 +143,11 @@ export function postProcessTimelineObjects( } export function postProcessAdLibPieces( - innerContext: RundownContext, - adLibPieces: IBlueprintAdLibPiece[], + innerContext: ICommonContext, blueprintId: BlueprintId, - partId?: PartId + rundownId: RundownId, + partId: PartId | undefined, + adLibPieces: IBlueprintAdLibPiece[] ): AdLibPiece[] { const span = profiler.startSpan('blueprints.postProcess.postProcessAdLibPieces') @@ -158,7 +158,7 @@ export function postProcessAdLibPieces( const piece: AdLibPiece = { ...itemOrig, _id: protectString(innerContext.getHashId(`${blueprintId}_${partId}_adlib_piece_${i++}`)), - rundownId: protectString(innerContext.rundown._id), + rundownId: rundownId, partId: partId, status: RundownAPI.PieceStatusCode.UNKNOWN, } @@ -190,58 +190,51 @@ export function postProcessAdLibPieces( } export function postProcessGlobalAdLibActions( - innerContext: RundownContext, - adlibActions: IBlueprintActionManifest[], - blueprintId: BlueprintId + innerContext: ICommonContext, + blueprintId: BlueprintId, + rundownId: RundownId, + adlibActions: IBlueprintActionManifest[] ): RundownBaselineAdLibAction[] { return adlibActions.map((action, i) => literal({ ...action, actionId: action.actionId, _id: protectString(innerContext.getHashId(`${blueprintId}_global_adlib_action_${i}`)), - rundownId: protectString(innerContext.rundownId), + rundownId: rundownId, partId: undefined, }) ) } export function postProcessAdLibActions( - innerContext: RundownContext, - adlibActions: IBlueprintActionManifest[], + innerContext: ICommonContext, blueprintId: BlueprintId, - partId: PartId + rundownId: RundownId, + partId: PartId, + adlibActions: IBlueprintActionManifest[] ): AdLibAction[] { return adlibActions.map((action, i) => literal({ ...action, actionId: action.actionId, _id: protectString(innerContext.getHashId(`${blueprintId}_${partId}_adlib_action_${i}`)), - rundownId: protectString(innerContext.rundownId), + rundownId: rundownId, partId: partId, }) ) } -export function postProcessStudioBaselineObjects(studio: Studio, objs: TSR.TSRTimelineObjBase[]): TimelineObjRundown[] { +export function postProcessStudioBaselineObjects( + context: StudioContext, + blueprintId: BlueprintId, + objs: TSR.TSRTimelineObjBase[] +): TimelineObjRundown[] { const timelineUniqueIds: { [id: string]: true } = {} - const context = new NotesContext( - 'studio', - 'studio', - false, - studio.blueprintId ? [unprotectString(studio.blueprintId)] : [] - ) - return postProcessTimelineObjects( - context, - protectString('studio'), - studio.blueprintId!, - objs, - false, - timelineUniqueIds - ) + return postProcessTimelineObjects(context, protectString('studio'), blueprintId, objs, false, timelineUniqueIds) } export function postProcessRundownBaselineItems( - innerContext: RundownContext, + innerContext: ICommonContext, blueprintId: BlueprintId, baselineItems: TSR.TSRTimelineObjBase[] ): TimelineObjGeneric[] { diff --git a/meteor/server/api/ingest/bucketAdlibs.ts b/meteor/server/api/ingest/bucketAdlibs.ts index 82cfbd55c4..7d1b438242 100644 --- a/meteor/server/api/ingest/bucketAdlibs.ts +++ b/meteor/server/api/ingest/bucketAdlibs.ts @@ -3,8 +3,8 @@ import { IngestAdlib } from 'tv-automation-sofie-blueprints-integration' import { ShowStyleCompound } from '../../../lib/collections/ShowStyleVariants' import { Studio } from '../../../lib/collections/Studios' import { loadShowStyleBlueprint } from '../blueprints/cache' -import { ShowStyleContext, NotesContext } from '../blueprints/context' -import { postProcessAdLibPieces, postProcessBucketAdLib } from '../blueprints/postProcess' +import { ShowStyleUserContext } from '../blueprints/context' +import { postProcessBucketAdLib } from '../blueprints/postProcess' import { RundownImportVersions } from '../../../lib/collections/Rundowns' import { PackageInfo } from '../../coreSystem' import { BucketAdLibs } from '../../../lib/collections/BucketAdlibs' @@ -14,8 +14,6 @@ import { cleanUpExpectedMediaItemForBucketAdLibPiece, updateExpectedMediaItemForBucketAdLibPiece, } from '../expectedMediaItems' -import { waitForPromise, unprotectString } from '../../../lib/lib' -import { initCacheForRundownPlaylist } from '../../DatabaseCaches' export function updateBucketAdlibFromIngestData( showStyle: ShowStyleCompound, @@ -25,21 +23,25 @@ export function updateBucketAdlibFromIngestData( ): PieceId | null { const { blueprint, blueprintId } = loadShowStyleBlueprint(showStyle) - const blueprintIds: Set = new Set() - if (blueprintId) { - blueprintIds.add(unprotectString(blueprintId)) - } - if (studio.blueprintId) { - blueprintIds.add(unprotectString(studio.blueprintId)) - } + // const blueprintIds: Set = new Set() + // if (blueprintId) { + // blueprintIds.add(unprotectString(blueprintId)) + // } + // if (studio.blueprintId) { + // blueprintIds.add(unprotectString(studio.blueprintId)) + // } - const context = new ShowStyleContext( + const context = new ShowStyleUserContext( + { + name: `Bucket Ad-Lib`, + identifier: `studioId=${studio._id},showStyleBaseId=${showStyle._id},showStyleVariantId=${showStyle.showStyleVariantId}`, + blackHoleUserNotes: true, // TODO-CONTEXT + }, studio, undefined, undefined, showStyle._id, - showStyle.showStyleVariantId, - new NotesContext('Bucket Ad-Lib', 'bucket-adlib', false, Array.from(blueprintIds)) + showStyle.showStyleVariantId ) if (!blueprint.getAdlibItem) throw new Meteor.Error(501, "This blueprint doesn't support ingest AdLibs") const rawAdlib = blueprint.getAdlibItem(context, ingestData) diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index 39231243b1..ed35db3b55 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -25,6 +25,7 @@ import { IngestSegment, IngestPart, BlueprintResultSegment, + ShowStyleBlueprintManifest, } from 'tv-automation-sofie-blueprints-integration' import { logger } from '../../../lib/logging' import { Studio, Studios } from '../../../lib/collections/Studios' @@ -38,7 +39,14 @@ import { produceRundownPlaylistInfo, } from '../rundown' import { loadShowStyleBlueprint } from '../blueprints/cache' -import { ShowStyleContext, RundownContext, SegmentContext, NotesContext } from '../blueprints/context' +import { + ShowStyleContext, + RundownContext, + StudioUserContext, + ShowStyleUserContext, + SegmentUserContext, + CommonContext, +} from '../blueprints/context' import { Blueprints, Blueprint, BlueprintId } from '../../../lib/collections/Blueprints' import { RundownBaselineObj, @@ -444,43 +452,52 @@ function updateRundownFromIngestData( const extendedIngestRundown = extendIngestRundownCore(ingestRundown, existingDbRundown) const rundownId = getRundownId(studio, ingestRundown.externalId) - const showStyle = selectShowStyleVariant(studio, extendedIngestRundown) + const selectShowStyleContext = new StudioUserContext( + { + name: 'selectShowStyleVariant', + identifier: `studioId=${studio._id},rundownId=${existingDbRundown?._id},ingestRundownId=${ingestRundown.externalId}`, + blackHoleUserNotes: true, + }, + studio + ) + // TODO-CONTEXT save any user notes from selectShowStyleContext + const showStyle = selectShowStyleVariant(selectShowStyleContext, extendedIngestRundown) if (!showStyle) { logger.debug('Blueprint rejected the rundown') throw new Meteor.Error(501, 'Blueprint rejected the rundown') } const showStyleBlueprint = loadShowStyleBlueprint(showStyle.base).blueprint - - const blueprintIds: Set = new Set() - if (showStyleBlueprint.blueprintId) { - blueprintIds.add(showStyleBlueprint.blueprintId) - } - if (studio.blueprintId) { - blueprintIds.add(unprotectString(studio.blueprintId)) - } - - const notesContext = new NotesContext( - `${showStyle.base.name}-${showStyle.variant.name}`, - `showStyleBaseId=${showStyle.base._id},showStyleVariantId=${showStyle.variant._id}`, - true, - Array.from(blueprintIds) - ) - const blueprintContext = new ShowStyleContext( + // const notesContext = new NotesContext(true) + const blueprintContext = new ShowStyleUserContext( + { + name: `${showStyle.base.name}-${showStyle.variant.name}`, + identifier: `showStyleBaseId=${showStyle.base._id},showStyleVariantId=${showStyle.variant._id}`, + }, studio, undefined, undefined, showStyle.base._id, - showStyle.variant._id, - notesContext + showStyle.variant._id ) const rundownRes = showStyleBlueprint.getRundown(blueprintContext, extendedIngestRundown) + const translationNamespaces: string[] = [] + if (showStyleBlueprint.blueprintId) { + translationNamespaces.push(showStyleBlueprint.blueprintId) + } + if (studio.blueprintId) { + translationNamespaces.push(unprotectString(studio.blueprintId)) + } + // Ensure the ids in the notes are clean - const rundownNotes = _.map(notesContext.getNotes(), (note) => + const rundownNotes = _.map(blueprintContext.notes, (note) => literal({ type: note.type, - message: note.message, + message: { + ...note.message, + namespaces: translationNamespaces, + }, origin: { name: `${showStyle.base.name}-${showStyle.variant.name}`, }, @@ -594,7 +611,16 @@ function updateRundownFromIngestData( } ) - const rundownPlaylistInfo = produceRundownPlaylistInfo(studio, dbRundownData, peripheralDevice) + const playlistContext = new StudioUserContext( + { + name: 'getRundownPlaylistInfo', + identifier: `studioId=${studio._id},rundownId=${existingDbRundown?._id},ingestRundownId=${ingestRundown.externalId}`, + blackHoleUserNotes: true, + }, + studio + ) + // TODO-CONTEXT save any user notes from playlistContext + const rundownPlaylistInfo = produceRundownPlaylistInfo(playlistContext, dbRundownData, peripheralDevice) const playlistChanges = saveIntoDb( RundownPlaylists, @@ -630,14 +656,10 @@ function updateRundownFromIngestData( const cache = waitForPromise(initCacheForRundownPlaylist(dbPlaylist)) // Save the baseline - - const rundownNotesContext = new NotesContext( - dbRundown.name, - `rundownId=${dbRundown._id}`, - true, - Array.from(blueprintIds) - ) - const blueprintRundownContext = new RundownContext(dbRundown, cache, rundownNotesContext) + const blueprintRundownContext = new CommonContext({ + name: dbRundown.name, + identifier: `rundownId=${dbRundown._id}`, + }) logger.info(`Building baseline objects for ${dbRundown._id}...`) logger.info(`... got ${rundownRes.baseline.length} objects from baseline.`) @@ -654,14 +676,17 @@ function updateRundownFromIngestData( logger.info(`... got ${rundownRes.globalAdLibPieces.length} adLib objects from baseline.`) const baselineAdlibPieces = postProcessAdLibPieces( blueprintRundownContext, - rundownRes.globalAdLibPieces, - showStyle.base.blueprintId + showStyle.base.blueprintId, + rundownId, + undefined, + rundownRes.globalAdLibPieces ) logger.info(`... got ${(rundownRes.globalActions || []).length} adLib actions from baseline.`) const baselineAdlibActions = postProcessGlobalAdLibActions( blueprintRundownContext, - rundownRes.globalActions || [], - showStyle.base.blueprintId + showStyle.base.blueprintId, + rundownId, + rundownRes.globalActions || [] ) // TODO - store notes from rundownNotesContext @@ -677,7 +702,7 @@ function updateRundownFromIngestData( const adlibActions: AdLibAction[] = [] const { blueprint, blueprintId } = loadShowStyleBlueprint(showStyle.base) - blueprintIds.add(unprotectString(blueprintId)) + // translationNamespaces.add(unprotectString(blueprintId)) _.each(ingestRundown.segments, (ingestSegment: IngestSegment) => { const segmentId = getSegmentId(rundownId, ingestSegment.externalId) @@ -686,22 +711,14 @@ function updateRundownFromIngestData( ingestSegment.parts = _.sortBy(ingestSegment.parts, (part) => part.rank) - const notesContext = new NotesContext( - ingestSegment.name, - `rundownId=${rundownId},segmentId=${segmentId}`, - true, - Array.from(blueprintIds) - ) - const context = new SegmentContext(dbRundown, cache, notesContext) - const res = blueprint.getSegment(context, ingestSegment) - const segmentContents = generateSegmentContents( - context, + cache, + dbRundown, + blueprint, blueprintId, ingestSegment, existingSegment, - existingParts, - res + existingParts ) segments.push(segmentContents.newSegment) parts.push(...segmentContents.parts) @@ -1139,30 +1156,14 @@ function updateSegmentFromIngestData( ingestSegment.parts = _.sortBy(ingestSegment.parts, (s) => s.rank) - const blueprintIds: Set = new Set() - if (blueprintId) { - blueprintIds.add(unprotectString(blueprintId)) - } - if (studio.blueprintId) { - blueprintIds.add(unprotectString(studio.blueprintId)) - } - - const notesContext = new NotesContext( - ingestSegment.name, - `rundownId=${rundown._id},segmentId=${segmentId}`, - true, - Array.from(blueprintIds) - ) - const context = new SegmentContext(rundown, cache, notesContext) - const res = blueprint.getSegment(context, ingestSegment) - const { parts, segmentPieces, adlibPieces, adlibActions, newSegment } = generateSegmentContents( - context, + cache, + rundown, + blueprint, blueprintId, ingestSegment, existingSegment, - existingParts, - res + existingParts ) const prepareSaveParts = prepareSaveIntoCache( @@ -1423,29 +1424,44 @@ export function handleUpdatedPartInner( } function generateSegmentContents( - context: SegmentContext, + cache: CacheForRundownPlaylist, + dbRundown: Rundown, + blueprint: ShowStyleBlueprintManifest, blueprintId: BlueprintId, ingestSegment: IngestSegment, existingSegment: DBSegment | undefined, - existingParts: DBPart[], - blueprintRes: BlueprintResultSegment + existingParts: DBPart[] ) { const span = profiler.startSpan('ingest.rundownInput.generateSegmentContents') - const rundownId = context._rundown._id + const rundownId = dbRundown._id const segmentId = getSegmentId(rundownId, ingestSegment.externalId) - const rawNotes = context.notesContext.getNotes() + + // const notesContext = new NotesContext(ingestSegment.name, `rundownId=${rundownId},segmentId=${segmentId}`, true) + const context = new SegmentUserContext( + { + name: `getSegment=${ingestSegment.name}`, + identifier: `rundownId=${rundownId},segmentId=${segmentId}`, + }, + dbRundown, + cache + ) + + const blueprintRes = blueprint.getSegment(context, ingestSegment) // Ensure all parts have a valid externalId set on them const knownPartIds = blueprintRes.parts.map((p) => p.part.externalId) const segmentNotes: SegmentNote[] = [] - for (const note of rawNotes) { + for (const note of context.notes) { if (!note.trackingId || knownPartIds.indexOf(note.trackingId) === -1) { segmentNotes.push( literal({ type: note.type, - message: note.message, + message: { + ...note.message, + namespaces: [unprotectString(blueprintId)], + }, origin: { name: '', // TODO }, @@ -1475,12 +1491,15 @@ function generateSegmentContents( const notes: PartNote[] = [] - for (const note of rawNotes) { + for (const note of context.notes) { if (note.trackingId === blueprintPart.part.externalId) { notes.push( literal({ type: note.type, - message: note.message, + message: { + ...note.message, + namespaces: [unprotectString(blueprintId)], + }, origin: { name: '', // TODO }, @@ -1520,8 +1539,12 @@ function generateSegmentContents( part.invalid ) ) - adlibPieces.push(...postProcessAdLibPieces(context, blueprintPart.adLibPieces, blueprintId, part._id)) - adlibActions.push(...postProcessAdLibActions(context, blueprintPart.actions || [], blueprintId, part._id)) + adlibPieces.push( + ...postProcessAdLibPieces(context, blueprintId, rundownId, part._id, blueprintPart.adLibPieces) + ) + adlibActions.push( + ...postProcessAdLibActions(context, blueprintId, rundownId, part._id, blueprintPart.actions || []) + ) }) // If the segment has no parts, then hide it diff --git a/meteor/server/api/playout/actions.ts b/meteor/server/api/playout/actions.ts index 054a047208..b3b3befd96 100644 --- a/meteor/server/api/playout/actions.ts +++ b/meteor/server/api/playout/actions.ts @@ -8,7 +8,7 @@ import { PeripheralDevices, PeripheralDevice } from '../../../lib/collections/Pe import { PeripheralDeviceAPI } from '../../../lib/api/peripheralDevice' import { getCurrentTime, getRandomId, waitForPromise } from '../../../lib/lib' import { loadShowStyleBlueprint } from '../blueprints/cache' -import { RundownContext } from '../blueprints/context' +import { RundownContext, RundownEventContext } from '../blueprints/context' import { setNextPart, onPartHasStoppedPlaying, @@ -83,8 +83,10 @@ export function activateRundownPlaylist( cache.defer((cache) => { if (!rundown) return // if the proper rundown hasn't been found, there's little point doing anything else - const { blueprint } = loadShowStyleBlueprint(waitForPromise(cache.activationCache.getShowStyleBase(rundown))) - const context = new RundownContext(rundown, cache, undefined) + const { blueprint, blueprintId } = loadShowStyleBlueprint( + waitForPromise(cache.activationCache.getShowStyleBase(rundown)) + ) + const context = new RundownEventContext(blueprintId, rundown, cache) context.wipeCache() if (blueprint.onRundownActivate) { Promise.resolve(blueprint.onRundownActivate(context)).catch(logger.error) @@ -98,13 +100,13 @@ export function deactivateRundownPlaylist(cache: CacheForRundownPlaylist, rundow cache.defer((cache) => { if (rundown) { - const { blueprint } = loadShowStyleBlueprint( + const { blueprint, blueprintId } = loadShowStyleBlueprint( waitForPromise(cache.activationCache.getShowStyleBase(rundown)) ) if (blueprint.onRundownDeActivate) { - Promise.resolve(blueprint.onRundownDeActivate(new RundownContext(rundown, cache, undefined))).catch( - logger.error - ) + Promise.resolve( + blueprint.onRundownDeActivate(new RundownEventContext(blueprintId, rundown, cache)) + ).catch(logger.error) } } }) diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index 41aafd2425..de4c3573d7 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -30,7 +30,6 @@ import { import { Blueprints } from '../../../lib/collections/Blueprints' import { RundownPlaylist, RundownPlaylists, RundownPlaylistId } from '../../../lib/collections/RundownPlaylists' import { loadShowStyleBlueprint } from '../blueprints/cache' -import { NotesContext } from '../blueprints/context/context' import { ActionExecutionContext, ActionPartChange } from '../blueprints/context/adlibActions' import { IngestActions } from '../ingest/actions' import { updateTimeline } from './timeline' @@ -1209,23 +1208,27 @@ export namespace ServerPlayoutAPI { if (!rundown) throw new Meteor.Error(501, `Current Rundown "${currentPartInstance.rundownId}" could not be found`) - const blueprintIds: Set = new Set() - if (studio.blueprintId) { - blueprintIds.add(unprotectString(studio.blueprintId)) - } - if (rundown.getShowStyleBase()?.blueprintId) { - blueprintIds.add(unprotectString(rundown.getShowStyleBase().blueprintId)) - } - - const notesContext = new NotesContext( - `${rundown.name}(${playlist.name})`, - `playlist=${playlist._id},rundown=${rundown._id},currentPartInstance=${ - currentPartInstance._id - },execution=${getRandomId()}`, - false, - Array.from(blueprintIds) + // const blueprintIds: Set = new Set() + // if (studio.blueprintId) { + // blueprintIds.add(unprotectString(studio.blueprintId)) + // } + // if (rundown.getShowStyleBase()?.blueprintId) { + // blueprintIds.add(unprotectString(rundown.getShowStyleBase().blueprintId)) + // } + + const actionContext = new ActionExecutionContext( + { + name: `${rundown.name}(${playlist.name})`, + identifier: `playlist=${playlist._id},rundown=${rundown._id},currentPartInstance=${ + currentPartInstance._id + },execution=${getRandomId()}`, + blackHoleUserNotes: true, // TODO-CONTEXT store these notes + }, + cache, + studio, + playlist, + rundown ) - const actionContext = new ActionExecutionContext(cache, notesContext, studio, playlist, rundown) // If any action cannot be done due to timings, that needs to be rejected by the context func(actionContext, cache, rundown, currentPartInstance) diff --git a/meteor/server/api/playout/take.ts b/meteor/server/api/playout/take.ts index a1f001a783..40fc6acc36 100644 --- a/meteor/server/api/playout/take.ts +++ b/meteor/server/api/playout/take.ts @@ -136,14 +136,16 @@ export function takeNextPartInnerSync( // beforeTake(rundown, previousPart || null, takePart) - const { blueprint } = waitForPromise(pBlueprint) + const { blueprint, blueprintId } = waitForPromise(pBlueprint) if (blueprint.onPreTake) { const span = profiler.startSpan('blueprint.onPreTake') try { waitForPromise( - Promise.resolve(blueprint.onPreTake(new PartEventContext(takeRundown, cache, takePartInstance))).catch( - logger.error - ) + Promise.resolve( + blueprint.onPreTake( + new PartEventContext('onPreTake', blueprintId, takeRundown, cache, takePartInstance) + ) + ).catch(logger.error) ) if (span) span.end() } catch (e) { @@ -160,7 +162,14 @@ export function takeNextPartInnerSync( const resolvedPieces = getResolvedPieces(cache, showStyle, previousPartInstance) const span = profiler.startSpan('blueprint.getEndStateForPart') - const context = new RundownContext(takeRundown, cache, undefined) + const context = new RundownContext( + { + name: `getEndStateForPart=${takeRundown.name}`, + identifier: `rundownId=${takeRundown._id},partInstanceId=${previousPartInstance._id}`, + }, + takeRundown, + cache + ) previousPartEndState = blueprint.getEndStateForPart( context, playlist.previousPersistentState, @@ -261,7 +270,15 @@ export function takeNextPartInnerSync( const span = profiler.startSpan('blueprint.onRundownFirstTake') waitForPromise( Promise.resolve( - blueprint.onRundownFirstTake(new PartEventContext(takeRundown, cache, takePartInstance)) + blueprint.onRundownFirstTake( + new PartEventContext( + 'onRundownFirstTake', + blueprintId, + takeRundown, + cache, + takePartInstance + ) + ) ).catch(logger.error) ) if (span) span.end() @@ -272,7 +289,9 @@ export function takeNextPartInnerSync( const span = profiler.startSpan('blueprint.onPostTake') waitForPromise( Promise.resolve( - blueprint.onPostTake(new PartEventContext(takeRundown, cache, takePartInstance)) + blueprint.onPostTake( + new PartEventContext('onPostTake', blueprintId, takeRundown, cache, takePartInstance) + ) ).catch(logger.error) ) if (span) span.end() diff --git a/meteor/server/api/playout/timeline.ts b/meteor/server/api/playout/timeline.ts index 4fc046f5e8..fd1b33fc43 100644 --- a/meteor/server/api/playout/timeline.ts +++ b/meteor/server/api/playout/timeline.ts @@ -46,7 +46,7 @@ import { RundownBaselineObj } from '../../../lib/collections/RundownBaselineObjs import * as _ from 'underscore' import { getLookeaheadObjects } from './lookahead' import { loadStudioBlueprint, loadShowStyleBlueprint } from '../blueprints/cache' -import { StudioContext, PartEventContext, TimelineEventContext } from '../blueprints/context' +import { StudioContext, TimelineEventContext } from '../blueprints/context' import { postProcessStudioBaselineObjects } from '../blueprints/postProcess' import { Part, PartId } from '../../../lib/collections/Parts' import { prefixAllObjectIds, getSelectedPartInstancesFromCache, getAllPieceInstancesFromCache } from './lib' @@ -262,7 +262,17 @@ function getTimelineRundown(cache: CacheForRundownPlaylist, studio: Studio): Tim const showStyleBlueprintManifest = showStyleBlueprint0.blueprint if (showStyleBlueprintManifest.onTimelineGenerate) { - const context = new TimelineEventContext(activeRundown, cache, currentPartInstance, nextPartInstance) + const context = new TimelineEventContext( + { + name: `onTimelineGenerate=${activeRundown.name}`, + identifier: `blueprintId=${showStyleBlueprint0.blueprintId},rundownId=${activeRundown._id},currentPartInstanceId=${currentPartInstance?._id},nextPartInstanceId=${nextPartInstance?._id}`, + blackHoleUserNotes: true, // TODO-CONTEXT store/show these notes + }, + activeRundown, + cache, + currentPartInstance, + nextPartInstance + ) const resolvedPieces = getResolvedPiecesFromFullTimeline(cache, playlist, timelineObjs) try { const tlGenRes = waitForPromise( @@ -305,8 +315,13 @@ function getTimelineRundown(cache: CacheForRundownPlaylist, studio: Studio): Tim const studioBlueprint = loadStudioBlueprint(studio) if (studioBlueprint) { const blueprint = studioBlueprint.blueprint - const baselineObjs = blueprint.getBaseline(new StudioContext(studio)) - studioBaseline = postProcessStudioBaselineObjects(studio, baselineObjs) + + const context = new StudioContext( + { name: 'studioBaseline', identifier: `studioId=${studio._id}` }, + studio + ) + const baselineObjs = blueprint.getBaseline(context) + studioBaseline = postProcessStudioBaselineObjects(context, studioBlueprint.blueprintId, baselineObjs) const id = `baseline_version` studioBaseline.push( diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index ac64cd09b7..afb7656f14 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -33,7 +33,7 @@ import { ShowStyleBases, ShowStyleBase, ShowStyleBaseId } from '../../lib/collec import { Blueprints } from '../../lib/collections/Blueprints' import { Studios, Studio } from '../../lib/collections/Studios' import { BlueprintResultOrderedRundowns, ExtendedIngestRundown } from 'tv-automation-sofie-blueprints-integration' -import { StudioConfigContext } from './blueprints/context' +import { StudioUserContext } from './blueprints/context' import { loadStudioBlueprint, loadShowStyleBlueprint } from './blueprints/cache' import { PackageInfo } from '../coreSystem' import { IngestActions } from './ingest/actions' @@ -66,9 +66,10 @@ import { triggerUpdateTimelineAfterIngestData } from './playout/playout' import { profiler } from './profiler' export function selectShowStyleVariant( - studio: Studio, + context: StudioUserContext, ingestRundown: ExtendedIngestRundown ): { variant: ShowStyleVariant; base: ShowStyleBase } | null { + const studio = context.getStudio() if (!studio.supportedShowStyleBase.length) { logger.debug(`Studio "${studio._id}" does not have any supportedShowStyleBase`) return null @@ -82,8 +83,6 @@ export function selectShowStyleVariant( return null } - const context = new StudioConfigContext(studio) - const studioBlueprint = loadStudioBlueprint(studio) if (!studioBlueprint) throw new Meteor.Error(500, `Studio "${studio._id}" does not have a blueprint`) @@ -139,10 +138,11 @@ export interface RundownPlaylistAndOrder { } export function produceRundownPlaylistInfo( - studio: Studio, + context: StudioUserContext, currentRundown: DBRundown, peripheralDevice: PeripheralDevice | undefined ): RundownPlaylistAndOrder { + const studio = context.getStudio() const studioBlueprint = loadStudioBlueprint(studio) if (!studioBlueprint) throw new Meteor.Error(500, `Studio "${studio._id}" does not have a blueprint`) @@ -157,7 +157,10 @@ export function produceRundownPlaylistInfo( `produceRundownPlaylistInfo: currentRundown ("${currentRundown._id}") not found in collection!` ) - const playlistInfo = studioBlueprint.blueprint.getRundownPlaylistInfo(unprotectObjectArray(allRundowns)) + const playlistInfo = studioBlueprint.blueprint.getRundownPlaylistInfo( + context, + unprotectObjectArray(allRundowns) + ) if (!playlistInfo) throw new Meteor.Error( 500, From 439748640843db04875d05bb0d9cea46f111a735 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 21 Sep 2020 17:19:55 +0100 Subject: [PATCH 20/58] fix: tests --- meteor/__mocks__/helpers/database.ts | 11 +- .../__tests__/context-adlibActions.test.ts | 15 +- .../api/blueprints/__tests__/context.test.ts | 208 ++++++++---------- .../blueprints/__tests__/postProcess.test.ts | 53 +++-- 4 files changed, 143 insertions(+), 144 deletions(-) diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index 848aefd2fc..bd240fd672 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -11,8 +11,6 @@ import { SourceLayerType, StudioBlueprintManifest, BlueprintManifestType, - IStudioContext, - IStudioConfigContext, IBlueprintShowStyleBase, IngestRundown, BlueprintManifestBase, @@ -22,7 +20,6 @@ import { BlueprintResultRundown, BlueprintResultSegment, IngestSegment, - SegmentContext, IBlueprintAdLibPiece, IBlueprintRundown, IBlueprintSegment, @@ -275,11 +272,11 @@ export function setupMockStudioBlueprint(showStyleBaseId: ShowStyleBaseId): Blue studioConfigManifest: [], studioMigrations: [], - getBaseline: (context: IStudioContext): TSR.TSRTimelineObjBase[] => { + getBaseline: (context: unknown): TSR.TSRTimelineObjBase[] => { return [] }, getShowStyleId: ( - context: IStudioConfigContext, + context: unknown, showStyles: Array, ingestRundown: IngestRundown ): string | null => { @@ -321,7 +318,7 @@ export function setupMockShowStyleBlueprint(showStyleVariantId: ShowStyleVariant showStyleConfigManifest: [], showStyleMigrations: [], getShowStyleVariantId: ( - context: IStudioConfigContext, + context: unknown, showStyleVariants: Array, ingestRundown: IngestRundown ): string | null => { @@ -341,7 +338,7 @@ export function setupMockShowStyleBlueprint(showStyleVariantId: ShowStyleVariant baseline: [], } }, - getSegment: (context: SegmentContext, ingestSegment: IngestSegment): BlueprintResultSegment => { + getSegment: (context: unknown, ingestSegment: IngestSegment): BlueprintResultSegment => { const segment: IBlueprintSegment = { name: ingestSegment.name ? ingestSegment.name : ingestSegment.externalId, metaData: ingestSegment.payload, diff --git a/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts b/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts index aeecfdcd63..a4c782b99b 100644 --- a/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts +++ b/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts @@ -7,7 +7,7 @@ import { import { protectString, unprotectString, waitForPromise, getRandomId, getCurrentTime } from '../../../../lib/lib' import { Studio, Studios } from '../../../../lib/collections/Studios' import { IBlueprintPart, IBlueprintPiece, PieceLifespan } from 'tv-automation-sofie-blueprints-integration' -import { NotesContext, ActionExecutionContext, ActionPartChange } from '../context' +import { ActionExecutionContext, ActionPartChange } from '../context' import { Rundown, Rundowns } from '../../../../lib/collections/Rundowns' import { PartInstance, PartInstanceId, PartInstances } from '../../../../lib/collections/PartInstances' import { @@ -107,14 +107,21 @@ describe('Test blueprint api context', () => { const rundownIds = getRundownIDsFromCache(cache, playlist) waitForPromise(cache.PieceInstances.fillWithDataFromDatabase({ rundownId: { $in: rundownIds } })) - const notesContext = new NotesContext('fakeContext', `fakeContext`, true) - const context = new ActionExecutionContext(cache, notesContext, studio, playlist, rundown) + const context = new ActionExecutionContext( + { + name: 'fakeContext', + identifier: 'action', + }, + cache, + studio, + playlist, + rundown + ) expect(context.getStudio()).toBeTruthy() return { playlist, rundown, - notesContext, context, } } diff --git a/meteor/server/api/blueprints/__tests__/context.test.ts b/meteor/server/api/blueprints/__tests__/context.test.ts index 9578ba7531..f9e7b263f3 100644 --- a/meteor/server/api/blueprints/__tests__/context.test.ts +++ b/meteor/server/api/blueprints/__tests__/context.test.ts @@ -2,22 +2,15 @@ import * as _ from 'underscore' import { setupDefaultStudioEnvironment, setupMockStudio, - setupDefaultRundown, DefaultEnvironment, setupDefaultRundownPlaylist, - setupMockStudioBlueprint, } from '../../../../__mocks__/helpers/database' -import { getHash, literal, protectString, unprotectObject, unprotectString, waitForPromise } from '../../../../lib/lib' +import { getHash, protectString, unprotectObject, unprotectString, waitForPromise } from '../../../../lib/lib' import { Studio } from '../../../../lib/collections/Studios' import { LookaheadMode, - NotesContext as INotesContext, - IBlueprintPart, - IBlueprintPartDB, IBlueprintAsRunLogEventContent, - IBlueprintSegment, IBlueprintSegmentDB, - IBlueprintPieceDB, TSR, IBlueprintPartInstance, IBlueprintPieceInstance, @@ -26,16 +19,7 @@ import { ConfigManifestEntry, SomeBlueprintManifest, } from 'tv-automation-sofie-blueprints-integration' -import { - CommonContext, - StudioConfigContext, - StudioContext, - ShowStyleContext, - NotesContext, - SegmentContext, - PartEventContext, - AsRunEventContext, -} from '../context' +import { CommonContext, StudioContext, ShowStyleContext, PartEventContext, AsRunEventContext } from '../context' import { ConfigRef } from '../config' import { ShowStyleBases } from '../../../../lib/collections/ShowStyleBases' import { ShowStyleVariant, ShowStyleVariants } from '../../../../lib/collections/ShowStyleVariants' @@ -43,10 +27,8 @@ import { Rundowns, Rundown, RundownId } from '../../../../lib/collections/Rundow import { DBPart, PartId } from '../../../../lib/collections/Parts' import { AsRunLogEvent, AsRunLog } from '../../../../lib/collections/AsRunLog' import { IngestDataCache, IngestCacheType } from '../../../../lib/collections/IngestDataCache' -import { Pieces } from '../../../../lib/collections/Pieces' import { wrapPartToTemporaryInstance, - PartInstance, PartInstances, unprotectPartInstance, } from '../../../../lib/collections/PartInstances' @@ -97,21 +79,21 @@ describe('Test blueprint api context', () => { describe('CommonContext', () => { testInFiber('no param', () => { - const context = new CommonContext('pre') + const context = new CommonContext({ name: 'name', identifier: 'pre' }) const res = context.getHashId(undefined as any) expect(res).toEqual(getHash('pre_hash0')) expect(context.unhashId(res)).toEqual('hash0') }) testInFiber('no param + notUnique', () => { - const context = new CommonContext('pre') + const context = new CommonContext({ name: 'name', identifier: 'pre' }) const res = context.getHashId(undefined as any, true) expect(res).toEqual(getHash('pre_hash0_1')) expect(context.unhashId(res)).toEqual('hash0_1') }) testInFiber('empty param', () => { - const context = new CommonContext('pre') + const context = new CommonContext({ name: 'name', identifier: 'pre' }) const res = context.getHashId('') expect(res).toEqual(getHash('pre_hash0')) @@ -124,7 +106,7 @@ describe('Test blueprint api context', () => { expect(res2).not.toEqual(res) }) testInFiber('string', () => { - const context = new CommonContext('pre') + const context = new CommonContext({ name: 'name', identifier: 'pre' }) const res = context.getHashId('something') expect(res).toEqual(getHash('pre_something')) @@ -137,7 +119,7 @@ describe('Test blueprint api context', () => { expect(res2).toEqual(res) }) testInFiber('string + notUnique', () => { - const context = new CommonContext('pre') + const context = new CommonContext({ name: 'name', identifier: 'pre' }) const res = context.getHashId('something', true) expect(res).toEqual(getHash('pre_something_0')) @@ -151,11 +133,7 @@ describe('Test blueprint api context', () => { }) }) - describe('NotesContext', () => { - // TODO - }) - - describe('StudioConfigContext', () => { + describe('StudioContext', () => { function mockStudio() { const manifest = () => ({ blueprintType: 'studio' as BlueprintManifestType.STUDIO, @@ -182,6 +160,7 @@ describe('Test blueprint api context', () => { required: false, }, ] as ConfigManifestEntry[], + studioMigrations: [], getBaseline: () => [], getShowStyleId: () => null, @@ -192,6 +171,13 @@ describe('Test blueprint api context', () => { sofieUrl: 'testUrl', mediaPreviewsUrl: '', }, + mappings: { + abc: { + deviceId: 'abc', + device: TSR.DeviceType.ABSTRACT, + lookahead: LookaheadMode.PRELOAD, + }, + }, blueprintConfig: { abc: true, '123': 'val2', notInManifest: 'val3' }, blueprintId: Blueprints.insert(blueprint), }) @@ -199,13 +185,13 @@ describe('Test blueprint api context', () => { testInFiber('getStudio', () => { const studio = mockStudio() - const context = new StudioConfigContext(studio) + const context = new StudioContext({ name: 'studio', identifier: unprotectString(studio._id) }, studio) expect(context.getStudio()).toEqual(studio) }) testInFiber('getStudioConfig', () => { const studio = mockStudio() - const context = new StudioConfigContext(studio) + const context = new StudioContext({ name: 'studio', identifier: unprotectString(studio._id) }, studio) expect(context.getStudioConfig()).toEqual({ SofieHostURL: 'testUrl', // Injected @@ -215,7 +201,7 @@ describe('Test blueprint api context', () => { }) testInFiber('getStudioConfigRef', () => { const studio = mockStudio() - const context = new StudioConfigContext(studio) + const context = new StudioContext({ name: 'studio', identifier: unprotectString(studio._id) }, studio) const getStudioConfigRef = jest.spyOn(ConfigRef, 'getStudioConfigRef') getStudioConfigRef.mockImplementation(() => { @@ -231,24 +217,10 @@ describe('Test blueprint api context', () => { getStudioConfigRef.mockRestore() } }) - }) - - describe('StudioContext', () => { - function mockStudio() { - return setupMockStudio({ - mappings: { - abc: { - deviceId: 'abc', - device: TSR.DeviceType.ABSTRACT, - lookahead: LookaheadMode.PRELOAD, - }, - }, - }) - } testInFiber('getStudioMappings', () => { const studio = mockStudio() - const context = new StudioContext(studio) + const context = new StudioContext({ name: 'studio', identifier: unprotectString(studio._id) }, studio) expect(context.getStudioMappings()).toEqual({ abc: { @@ -364,41 +336,19 @@ describe('Test blueprint api context', () => { ) Blueprints.update(blueprint._id, blueprint) - const notesContext = new NotesContext( - contextName || 'N/A', - `rundownId=${rundownId},segmentId=${segmentId}`, - false - ) return new ShowStyleContext( + { + name: contextName || 'N/A', + identifier: `rundownId=${rundownId},segmentId=${segmentId}`, + }, studio, undefined, undefined, showStyleVariant.showStyleBaseId, - showStyleVariant._id, - notesContext + showStyleVariant._id ) } - testInFiber('handleNotesExternally', () => { - const studio = mockStudio() - const context = getContext(studio) - const notesContext: NotesContext = context.notesContext - expect(notesContext).toBeTruthy() - - expect(notesContext.handleNotesExternally).toEqual(context.handleNotesExternally) - expect(notesContext.handleNotesExternally).toBeFalsy() - - // set to true - context.handleNotesExternally = true - expect(notesContext.handleNotesExternally).toEqual(context.handleNotesExternally) - expect(notesContext.handleNotesExternally).toBeTruthy() - - // and back to false - context.handleNotesExternally = false - expect(notesContext.handleNotesExternally).toEqual(context.handleNotesExternally) - expect(notesContext.handleNotesExternally).toBeFalsy() - }) - testInFiber('getShowStyleBase', () => { const studio = mockStudio() const context = getContext(studio) @@ -480,50 +430,52 @@ describe('Test blueprint api context', () => { } }) - class FakeNotesContext implements INotesContext { - error: (message: string) => void = jest.fn() - warning: (message: string) => void = jest.fn() - getHashId: (originString: string, originIsNotUnique?: boolean | undefined) => string = jest.fn( - () => 'hashed' - ) - unhashId: (hash: string) => string = jest.fn(() => 'unhash') - } - - testInFiber('notes', () => { - const studio = mockStudio() - const context = getContext(studio) - - // Fake the notes context - const fakeNotes = new FakeNotesContext() - // Apply mocked notesContext: - ;(context as any).notesContext = fakeNotes - - context.error('this is an {{error}}', { error: 'embarrasing situation' }, 'extid1') - - expect(fakeNotes.error).toHaveBeenCalledTimes(1) - expect(fakeNotes.error).toHaveBeenCalledWith( - 'this is an {{error}}', - { error: 'embarrasing situation' }, - 'extid1' - ) - - context.warning('this is an warning', {}, 'extid1') - expect(fakeNotes.warning).toHaveBeenCalledTimes(1) - expect(fakeNotes.warning).toHaveBeenCalledWith('this is an warning', {}, 'extid1') - - const hash = context.getHashId('str 1', false) - expect(hash).toEqual('hashed') - expect(fakeNotes.getHashId).toHaveBeenCalledTimes(1) - expect(fakeNotes.getHashId).toHaveBeenCalledWith('str 1', false) - - const unhash = context.unhashId('str 1') - expect(unhash).toEqual('unhash') - expect(fakeNotes.unhashId).toHaveBeenCalledTimes(1) - expect(fakeNotes.unhashId).toHaveBeenCalledWith('str 1') - }) + // class FakeNotesContext implements INotesContext { + // error: (message: string) => void = jest.fn() + // warning: (message: string) => void = jest.fn() + // getHashId: (originString: string, originIsNotUnique?: boolean | undefined) => string = jest.fn( + // () => 'hashed' + // ) + // unhashId: (hash: string) => string = jest.fn(() => 'unhash') + // } + + // testInFiber('notes', () => { + // const studio = mockStudio() + // const context = getContext(studio) + + // // Fake the notes context + // const fakeNotes = new FakeNotesContext() + // // Apply mocked notesContext: + // ;(context as any).notesContext = fakeNotes + + // context.error('this is an {{error}}', { error: 'embarrasing situation' }, 'extid1') + + // expect(fakeNotes.error).toHaveBeenCalledTimes(1) + // expect(fakeNotes.error).toHaveBeenCalledWith( + // 'this is an {{error}}', + // { error: 'embarrasing situation' }, + // 'extid1' + // ) + + // context.warning('this is an warning', {}, 'extid1') + // expect(fakeNotes.warning).toHaveBeenCalledTimes(1) + // expect(fakeNotes.warning).toHaveBeenCalledWith('this is an warning', {}, 'extid1') + + // const hash = context.getHashId('str 1', false) + // expect(hash).toEqual('hashed') + // expect(fakeNotes.getHashId).toHaveBeenCalledTimes(1) + // expect(fakeNotes.getHashId).toHaveBeenCalledWith('str 1', false) + + // const unhash = context.unhashId('str 1') + // expect(unhash).toEqual('unhash') + // expect(fakeNotes.unhashId).toHaveBeenCalledTimes(1) + // expect(fakeNotes.unhashId).toHaveBeenCalledWith('str 1') + // }) }) - describe('SegmentContext', () => {}) + describe('SegmentUserContext', () => { + // TODO? + }) describe('PartEventContext', () => { testInFiber('get part', () => { @@ -541,7 +493,7 @@ describe('Test blueprint api context', () => { } const tmpPart = wrapPartToTemporaryInstance(mockPart as DBPart) - const context = new PartEventContext(rundown, cache, tmpPart) + const context = new PartEventContext('fake', protectString('blueprint1'), rundown, cache, tmpPart) expect(context.getStudio()).toBeTruthy() expect(context.part).toEqual(tmpPart) @@ -565,7 +517,15 @@ describe('Test blueprint api context', () => { let cache = waitForPromise(initCacheForRundownPlaylist(playlist)) - return new AsRunEventContext(rundown, cache, mockEvent) + return new AsRunEventContext( + { + name: 'as-run', + identifier: unprotectString(mockEvent._id), + }, + rundown, + cache, + mockEvent + ) } testInFiber('getAllAsRunEvents', () => { const { rundownId } = setupDefaultRundownPlaylist(env) @@ -586,7 +546,15 @@ describe('Test blueprint api context', () => { content: IBlueprintAsRunLogEventContent.STARTEDPLAYBACK, } - const context = new AsRunEventContext(rundown, cache, mockEvent) + const context = new AsRunEventContext( + { + name: 'as-run', + identifier: unprotectString(mockEvent._id), + }, + rundown, + cache, + mockEvent + ) expect(context.getStudio()).toBeTruthy() expect(context.asRunEvent).toEqual(mockEvent) diff --git a/meteor/server/api/blueprints/__tests__/postProcess.test.ts b/meteor/server/api/blueprints/__tests__/postProcess.test.ts index 74cf8ea165..81395c4878 100644 --- a/meteor/server/api/blueprints/__tests__/postProcess.test.ts +++ b/meteor/server/api/blueprints/__tests__/postProcess.test.ts @@ -10,13 +10,14 @@ import { postProcessAdLibPieces, postProcessPieces, } from '../postProcess' -import { RundownContext, NotesContext } from '../context' +import { RundownContext, CommonContext, StudioContext } from '../context' import { IBlueprintPiece, IBlueprintAdLibPiece, TimelineObjectCoreExt, TSR, PieceLifespan, + IUserNotesContext, } from 'tv-automation-sofie-blueprints-integration' import { Piece } from '../../../../lib/collections/Pieces' import { TimelineObjGeneric, TimelineObjType } from '../../../../lib/collections/Timeline' @@ -74,8 +75,22 @@ describe('Test blueprint post-process', () => { }) let cache = waitForPromise(initCacheForRundownPlaylist(playlist)) - const rundownNotesContext = new NotesContext(rundown.name, `rundownId=${rundown._id}`, true) - return new RundownContext(rundown, cache, rundownNotesContext) + const context = new RundownContext( + { name: rundown.name, identifier: `rundownId=${rundown._id}` }, + rundown, + cache + ) + + // Make sure we arent an IUserNotesContext, as that means new work to handle those notes + expect(((context as unknown) as IUserNotesContext).userError).toBeUndefined() + return context + } + function getStudioContext(studio: Studio) { + const context = new StudioContext({ name: studio.name, identifier: `studioId=${studio._id}` }, studio) + + // Make sure we arent an IUserNotesContext, as that means new work to handle those notes + expect(((context as unknown) as IUserNotesContext).userError).toBeUndefined() + return context } function ensureAllKeysDefined(template: T, objects: T[]) { @@ -95,14 +110,18 @@ describe('Test blueprint post-process', () => { describe('postProcessStudioBaselineObjects', () => { testInFiber('no objects', () => { const studio = getStudio() + const context = getStudioContext(studio) + const blueprintId = protectString('blueprint0') // Ensure that an empty array works ok - const res = postProcessStudioBaselineObjects(studio, []) + const res = postProcessStudioBaselineObjects(context, blueprintId, []) expect(res).toHaveLength(0) }) testInFiber('some no ids', () => { const studio = getStudio() + const context = getStudioContext(studio) + const blueprintId = protectString('blueprint0') const rawObjects = literal([ { @@ -141,7 +160,7 @@ describe('Test blueprint post-process', () => { // TODO - mock getHash? - const res = postProcessStudioBaselineObjects(studio, _.clone(rawObjects)) + const res = postProcessStudioBaselineObjects(context, blueprintId, _.clone(rawObjects)) // Nothing should have been overridden (yet) _.each(rawObjects, (obj) => { @@ -160,6 +179,8 @@ describe('Test blueprint post-process', () => { }) testInFiber('duplicate ids', () => { const studio = getStudio() + const context = getStudioContext(studio) + const blueprintId = protectString('blueprint0') const rawObjects = literal([ { @@ -197,11 +218,11 @@ describe('Test blueprint post-process', () => { ]) try { - postProcessStudioBaselineObjects(studio, _.clone(rawObjects)) + postProcessStudioBaselineObjects(context, blueprintId, _.clone(rawObjects)) fail('expected to throw') } catch (e) { expect(e.message).toBe( - `[400] Error in blueprint "${studio.blueprintId}": ids of timelineObjs must be unique! ("testObj")` + `[400] Error in blueprint "${blueprintId}": ids of timelineObjs must be unique! ("testObj")` ) } }) @@ -343,15 +364,19 @@ describe('Test blueprint post-process', () => { describe('postProcessAdLibPieces', () => { testInFiber('no pieces', () => { - const context = getContext() + const context = getStudioContext(getStudio()) + const blueprintId = protectString('blueprint0') + const rundownId = protectString('rundown1') // Ensure that an empty array works ok - const res = postProcessAdLibPieces(context, [], protectString('blueprint9')) + const res = postProcessAdLibPieces(context, blueprintId, rundownId, undefined, []) expect(res).toHaveLength(0) }) testInFiber('various pieces', () => { - const context = getContext() + const context = getStudioContext(getStudio()) + const blueprintId = protectString('blueprint9') + const rundownId = protectString('rundown1') const pieces = literal([ { @@ -400,7 +425,7 @@ describe('Test blueprint post-process', () => { const expectedIds = _.clone(mockedIds) jest.spyOn(context, 'getHashId').mockImplementation(() => mockedIds.shift() || '') - const res = postProcessAdLibPieces(context, pieces, protectString('blueprint9')) + const res = postProcessAdLibPieces(context, blueprintId, rundownId, undefined, pieces) // expect(res).toHaveLength(3) expect(res).toMatchObject(pieces.map((p) => _.omit(p, '_id'))) @@ -430,7 +455,9 @@ describe('Test blueprint post-process', () => { expect(ids).toEqual(expectedIds.sort()) }) testInFiber('piece with content', () => { - const context = getContext() + const context = getStudioContext(getStudio()) + const blueprintId = protectString('blueprint0') + const rundownId = protectString('rundown1') const piece = literal({ _rank: 9, @@ -453,7 +480,7 @@ describe('Test blueprint post-process', () => { lifespan: PieceLifespan.WithinPart, }) - const res = postProcessAdLibPieces(context, [piece], protectString('blueprint9')) + const res = postProcessAdLibPieces(context, blueprintId, rundownId, undefined, [piece]) expect(res).toHaveLength(1) expect(res).toMatchObject([piece]) From fc80552a74f92390fe28eb22d198078f6329b399 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 25 Sep 2020 14:10:17 +0100 Subject: [PATCH 21/58] chore: update blueprints-integration --- meteor/__mocks__/helpers/database.ts | 4 +- meteor/package-lock.json | 6 +-- meteor/package.json | 2 +- .../blueprints/__tests__/postProcess.test.ts | 4 +- .../api/blueprints/context/adlibActions.ts | 8 ++-- .../server/api/blueprints/context/context.ts | 42 ++++++++----------- 6 files changed, 30 insertions(+), 36 deletions(-) diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index bd240fd672..0be8987fbf 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -16,7 +16,7 @@ import { BlueprintManifestBase, ShowStyleBlueprintManifest, IBlueprintShowStyleVariant, - ShowStyleContext, + IShowStyleContext, BlueprintResultRundown, BlueprintResultSegment, IngestSegment, @@ -324,7 +324,7 @@ export function setupMockShowStyleBlueprint(showStyleVariantId: ShowStyleVariant ): string | null => { return SHOW_STYLE_VARIANT_ID }, - getRundown: (context: ShowStyleContext, ingestRundown: IngestRundown): BlueprintResultRundown => { + getRundown: (context: IShowStyleContext, ingestRundown: IngestRundown): BlueprintResultRundown => { const rundown: IBlueprintRundown = { externalId: ingestRundown.externalId, name: ingestRundown.name, diff --git a/meteor/package-lock.json b/meteor/package-lock.json index b1b06b55a9..61ad9e61b5 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17462,9 +17462,9 @@ } }, "tv-automation-sofie-blueprints-integration": { - "version": "3.0.0-nightly-feat-localisation-20200925-131650-a7a0fd91.0", - "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-localisation-20200925-131650-a7a0fd91.0.tgz", - "integrity": "sha512-EtjPu0dY7C0CkngNN+Y1Pp9MHtO5CIAAg9zkMW84u46p7Mul27s6t6y8yyGIiYPH805AUADCsxDjK6/Vu4W0gA==", + "version": "3.0.0-nightly-feat-restructure-logging-20200925-131814-2f27114c.0", + "resolved": "https://registry.npmjs.org/tv-automation-sofie-blueprints-integration/-/tv-automation-sofie-blueprints-integration-3.0.0-nightly-feat-restructure-logging-20200925-131814-2f27114c.0.tgz", + "integrity": "sha512-HapGf5m+H5aeLRTi/4Gl21lq8FwXcVE6hkxtwJ6g7AHWNoFz32/azZBCPigy9QFEBBLtNwOqS/FwJhcSNbOQjA==", "requires": { "moment": "2.22.2", "timeline-state-resolver-types": "5.0.0-nightly-release24-20200914-120931-3357b9dc.0", diff --git a/meteor/package.json b/meteor/package.json index 3d9f7fa504..aa757e6e37 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -95,7 +95,7 @@ "soap": "^0.31.0", "superfly-timeline": "^8.1.1", "timecode": "0.0.4", - "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-localisation-20200925-131650-a7a0fd91.0", + "tv-automation-sofie-blueprints-integration": "3.0.0-nightly-feat-restructure-logging-20200925-131814-2f27114c.0", "underscore": "^1.11.0", "utility-types": "^3.10.0", "velocity-animate": "^1.5.2", diff --git a/meteor/server/api/blueprints/__tests__/postProcess.test.ts b/meteor/server/api/blueprints/__tests__/postProcess.test.ts index 81395c4878..6f4979ec84 100644 --- a/meteor/server/api/blueprints/__tests__/postProcess.test.ts +++ b/meteor/server/api/blueprints/__tests__/postProcess.test.ts @@ -82,14 +82,14 @@ describe('Test blueprint post-process', () => { ) // Make sure we arent an IUserNotesContext, as that means new work to handle those notes - expect(((context as unknown) as IUserNotesContext).userError).toBeUndefined() + expect(((context as unknown) as IUserNotesContext).notifyUserError).toBeUndefined() return context } function getStudioContext(studio: Studio) { const context = new StudioContext({ name: studio.name, identifier: `studioId=${studio._id}` }, studio) // Make sure we arent an IUserNotesContext, as that means new work to handle those notes - expect(((context as unknown) as IUserNotesContext).userError).toBeUndefined() + expect(((context as unknown) as IUserNotesContext).notifyUserError).toBeUndefined() return context } diff --git a/meteor/server/api/blueprints/context/adlibActions.ts b/meteor/server/api/blueprints/context/adlibActions.ts index 95077450e5..7a12ffeeae 100644 --- a/meteor/server/api/blueprints/context/adlibActions.ts +++ b/meteor/server/api/blueprints/context/adlibActions.ts @@ -13,8 +13,8 @@ import { import { Part } from '../../../../lib/collections/Parts' import { logger } from '../../../../lib/logging' import { - EventContext as IEventContext, - ActionExecutionContext as IActionExecutionContext, + IEventContext, + IActionExecutionContext, IBlueprintPartInstance, IBlueprintPieceInstance, IBlueprintPiece, @@ -125,7 +125,7 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE this.takeAfterExecute = false } - userError(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserError(message: string, params?: { [key: string]: any }, trackingId?: string): void { if (this.blackHoleNotes) { this.logError(message) } else { @@ -139,7 +139,7 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE }) } } - userWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { if (this.blackHoleNotes) { this.logWarning(message) } else { diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index b1e6ebc6c9..9110856c16 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -19,15 +19,15 @@ import { logger } from '../../../../lib/logging' import { ICommonContext, IUserNotesContext, - ShowStyleContext as IShowStyleContext, - RundownContext as IRundownContext, - SegmentUserContext as ISegmentUserContext, - EventContext as IEventContext, - AsRunEventContext as IAsRunEventContext, - PartEventContext as IPartEventContext, - TimelineEventContext as ITimelineEventContext, - StudioContext as IStudioContext, - StudioUserContext as IStudioUserContext, + IShowStyleContext, + IRundownContext, + ISegmentUserContext, + IEventContext, + IAsRunEventContext, + IPartEventContext, + ITimelineEventContext, + IStudioContext, + IStudioUserContext, BlueprintMappings, IBlueprintSegmentDB, IngestPart, @@ -38,7 +38,6 @@ import { IBlueprintAsRunLogEvent, IBlueprintExternalMessageQueueObj, ExtendedIngestRundown, - ShowStyleBlueprintManifest, } from 'tv-automation-sofie-blueprints-integration' import { Studio, StudioId, Studios } from '../../../../lib/collections/Studios' import { ConfigRef, preprocessStudioConfig, findMissingConfigs, preprocessShowStyleConfig } from '../config' @@ -187,7 +186,7 @@ export class StudioUserContext extends StudioContext implements IStudioUserConte this.blackHoleNotes = contextInfo.blackHoleUserNotes ?? false } - userError(message: string, params?: { [key: string]: any }): void { + notifyUserError(message: string, params?: { [key: string]: any }): void { if (this.blackHoleNotes) { this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { @@ -200,7 +199,7 @@ export class StudioUserContext extends StudioContext implements IStudioUserConte }) } } - userWarning(message: string, params?: { [key: string]: any }): void { + notifyUserWarning(message: string, params?: { [key: string]: any }): void { if (this.blackHoleNotes) { this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { @@ -311,7 +310,7 @@ export class ShowStyleUserContext extends ShowStyleContext implements IUserNotes super(contextInfo, studio, cache, _rundown, showStyleBaseId, showStyleVariantId) } - userError(message: string, params?: { [key: string]: any }): void { + notifyUserError(message: string, params?: { [key: string]: any }): void { if (this.blackHoleNotes) { this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { @@ -324,7 +323,7 @@ export class ShowStyleUserContext extends ShowStyleContext implements IUserNotes }) } } - userWarning(message: string, params?: { [key: string]: any }): void { + notifyUserWarning(message: string, params?: { [key: string]: any }): void { if (this.blackHoleNotes) { this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { @@ -388,7 +387,7 @@ export class SegmentUserContext extends RundownContext implements ISegmentUserCo super(contextInfo, rundown, cache) } - userError(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserError(message: string, params?: { [key: string]: any }, trackingId?: string): void { this.notes.push({ type: NoteType.ERROR, message: { @@ -398,7 +397,7 @@ export class SegmentUserContext extends RundownContext implements ISegmentUserCo trackingId: trackingId, }) } - userWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { this.notes.push({ type: NoteType.WARNING, message: { @@ -473,7 +472,7 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve return getCurrentTime() } - userError(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserError(message: string, params?: { [key: string]: any }, trackingId?: string): void { if (this.blackHoleNotes) { this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { @@ -487,7 +486,7 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve }) } } - userWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { if (this.blackHoleNotes) { this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { @@ -512,12 +511,7 @@ export class AsRunEventContext extends RundownContext implements IAsRunEventCont cache: ReadOnlyCacheForRundownPlaylist, asRunEvent: AsRunLogEvent ) { - super( - contextInfo, - rundown, - cache - // new NotesContext(rundown.name, `rundownId=${rundown._id},asRunEventId=${asRunEvent._id}`, false) - ) + super(contextInfo, rundown, cache) this.asRunEvent = unprotectObject(asRunEvent) } From ecae880c296edf72045a35ad65283d532216536e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 25 Sep 2020 14:36:06 +0100 Subject: [PATCH 22/58] chore: remove old test --- .../api/blueprints/__tests__/context.test.ts | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/meteor/server/api/blueprints/__tests__/context.test.ts b/meteor/server/api/blueprints/__tests__/context.test.ts index f9e7b263f3..f768536b9c 100644 --- a/meteor/server/api/blueprints/__tests__/context.test.ts +++ b/meteor/server/api/blueprints/__tests__/context.test.ts @@ -429,48 +429,6 @@ describe('Test blueprint api context', () => { getShowStyleConfigRef.mockRestore() } }) - - // class FakeNotesContext implements INotesContext { - // error: (message: string) => void = jest.fn() - // warning: (message: string) => void = jest.fn() - // getHashId: (originString: string, originIsNotUnique?: boolean | undefined) => string = jest.fn( - // () => 'hashed' - // ) - // unhashId: (hash: string) => string = jest.fn(() => 'unhash') - // } - - // testInFiber('notes', () => { - // const studio = mockStudio() - // const context = getContext(studio) - - // // Fake the notes context - // const fakeNotes = new FakeNotesContext() - // // Apply mocked notesContext: - // ;(context as any).notesContext = fakeNotes - - // context.error('this is an {{error}}', { error: 'embarrasing situation' }, 'extid1') - - // expect(fakeNotes.error).toHaveBeenCalledTimes(1) - // expect(fakeNotes.error).toHaveBeenCalledWith( - // 'this is an {{error}}', - // { error: 'embarrasing situation' }, - // 'extid1' - // ) - - // context.warning('this is an warning', {}, 'extid1') - // expect(fakeNotes.warning).toHaveBeenCalledTimes(1) - // expect(fakeNotes.warning).toHaveBeenCalledWith('this is an warning', {}, 'extid1') - - // const hash = context.getHashId('str 1', false) - // expect(hash).toEqual('hashed') - // expect(fakeNotes.getHashId).toHaveBeenCalledTimes(1) - // expect(fakeNotes.getHashId).toHaveBeenCalledWith('str 1', false) - - // const unhash = context.unhashId('str 1') - // expect(unhash).toEqual('unhash') - // expect(fakeNotes.unhashId).toHaveBeenCalledTimes(1) - // expect(fakeNotes.unhashId).toHaveBeenCalledWith('str 1') - // }) }) describe('SegmentUserContext', () => { From ef2ca49a68c2d7c39ffef72698f2e60cbb1dc545 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Fri, 25 Sep 2020 14:40:29 +0100 Subject: [PATCH 23/58] fix: improve log messages --- meteor/server/api/blueprints/context/context.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index 9110856c16..33241fed56 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -73,11 +73,14 @@ export interface UserContextInfo extends ContextInfo { export class CommonContext implements ICommonContext { private readonly _contextIdentifier: string + private readonly _contextName: string + private hashI = 0 private hashed: { [hash: string]: string } = {} constructor(info: ContextInfo) { this._contextIdentifier = info.identifier + this._contextName = info.name } getHashId(str: string, isNotUnique?: boolean) { if (!str) str = 'hash' + this.hashI++ @@ -95,20 +98,16 @@ export class CommonContext implements ICommonContext { } logDebug(message: string): void { - // TODO - prefix with _contextIdentifier? - logger.debug(message) + logger.debug(`"${this._contextName}": "${message}"\n(${this._contextIdentifier})`) } logInfo(message: string): void { - // TODO - prefix with _contextIdentifier? - logger.info(message) + logger.info(`"${this._contextName}": "${message}"\n(${this._contextIdentifier})`) } logWarning(message: string): void { - // TODO - prefix with _contextIdentifier? - logger.warn(message) + logger.warn(`"${this._contextName}": "${message}"\n(${this._contextIdentifier})`) } logError(message: string): void { - // TODO - prefix with _contextIdentifier? - logger.error(message) + logger.error(`"${this._contextName}": "${message}"\n(${this._contextIdentifier})`) } } From 81db465ffad54c96283fb5867d69897e140186bb Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 28 Sep 2020 11:47:36 +0100 Subject: [PATCH 24/58] chore: review comments --- meteor/server/api/blueprints/context/adlibActions.ts | 2 ++ meteor/server/api/ingest/bucketAdlibs.ts | 8 -------- meteor/server/api/playout/playout.ts | 8 -------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/meteor/server/api/blueprints/context/adlibActions.ts b/meteor/server/api/blueprints/context/adlibActions.ts index 7a12ffeeae..8ea3240336 100644 --- a/meteor/server/api/blueprints/context/adlibActions.ts +++ b/meteor/server/api/blueprints/context/adlibActions.ts @@ -123,6 +123,8 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE this.rundownPlaylist = rundownPlaylist this.rundown = rundown this.takeAfterExecute = false + + this.blackHoleNotes = contextInfo.blackHoleUserNotes ?? false } notifyUserError(message: string, params?: { [key: string]: any }, trackingId?: string): void { diff --git a/meteor/server/api/ingest/bucketAdlibs.ts b/meteor/server/api/ingest/bucketAdlibs.ts index 7d1b438242..ab6052f174 100644 --- a/meteor/server/api/ingest/bucketAdlibs.ts +++ b/meteor/server/api/ingest/bucketAdlibs.ts @@ -23,14 +23,6 @@ export function updateBucketAdlibFromIngestData( ): PieceId | null { const { blueprint, blueprintId } = loadShowStyleBlueprint(showStyle) - // const blueprintIds: Set = new Set() - // if (blueprintId) { - // blueprintIds.add(unprotectString(blueprintId)) - // } - // if (studio.blueprintId) { - // blueprintIds.add(unprotectString(studio.blueprintId)) - // } - const context = new ShowStyleUserContext( { name: `Bucket Ad-Lib`, diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index de4c3573d7..81b04c6c0f 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -1208,14 +1208,6 @@ export namespace ServerPlayoutAPI { if (!rundown) throw new Meteor.Error(501, `Current Rundown "${currentPartInstance.rundownId}" could not be found`) - // const blueprintIds: Set = new Set() - // if (studio.blueprintId) { - // blueprintIds.add(unprotectString(studio.blueprintId)) - // } - // if (rundown.getShowStyleBase()?.blueprintId) { - // blueprintIds.add(unprotectString(rundown.getShowStyleBase().blueprintId)) - // } - const actionContext = new ActionExecutionContext( { name: `${rundown.name}(${playlist.name})`, From 07de3bd637ecb8af90994b06365b56a25a64a6c7 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 5 Oct 2020 11:06:43 +0100 Subject: [PATCH 25/58] chore: review comments --- meteor/package-lock.json | 2 +- .../api/blueprints/context/adlibActions.ts | 20 ++++--- .../server/api/blueprints/context/context.ts | 52 +++++++++---------- meteor/server/api/ingest/bucketAdlibs.ts | 2 +- meteor/server/api/ingest/rundownInput.ts | 10 ++-- meteor/server/api/playout/playout.ts | 2 +- meteor/server/api/playout/timeline.ts | 2 +- 7 files changed, 44 insertions(+), 46 deletions(-) diff --git a/meteor/package-lock.json b/meteor/package-lock.json index 61ad9e61b5..45d70a16c4 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -17096,7 +17096,7 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, diff --git a/meteor/server/api/blueprints/context/adlibActions.ts b/meteor/server/api/blueprints/context/adlibActions.ts index 8ea3240336..f424ffe097 100644 --- a/meteor/server/api/blueprints/context/adlibActions.ts +++ b/meteor/server/api/blueprints/context/adlibActions.ts @@ -33,12 +33,12 @@ import { PartInstanceId, PartInstance } from '../../../../lib/collections/PartIn import { CacheForRundownPlaylist } from '../../../DatabaseCaches' import { getResolvedPieces } from '../../playout/pieces' import { postProcessPieces, postProcessTimelineObjects } from '../postProcess' -import { ShowStyleContext, ContextInfo, UserContextInfo, RawNote } from './context' +import { ShowStyleContext, UserContextInfo } from './context' import { isTooCloseToAutonext } from '../../playout/lib' import { ServerPlayoutAdLibAPI } from '../../playout/adlib' import { MongoQuery } from '../../../../lib/typings/meteor' import { clone } from '../../../../lib/lib' -import { NoteType } from '../../../../lib/api/notes' +import { NoteType, INoteBase } from '../../../../lib/api/notes' export enum ActionPartChange { NONE = 0, @@ -100,8 +100,8 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE private readonly rundownPlaylist: RundownPlaylist private readonly rundown: Rundown - public readonly notes: RawNote[] = [] - private readonly blackHoleNotes: boolean + public readonly notes: INoteBase[] = [] + private readonly tempSendNotesIntoBlackHole: boolean private queuedPartInstance: PartInstance | undefined @@ -124,11 +124,11 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE this.rundown = rundown this.takeAfterExecute = false - this.blackHoleNotes = contextInfo.blackHoleUserNotes ?? false + this.tempSendNotesIntoBlackHole = contextInfo.tempSendUserNotesIntoBlackHole ?? false } - notifyUserError(message: string, params?: { [key: string]: any }, trackingId?: string): void { - if (this.blackHoleNotes) { + notifyUserError(message: string, params?: { [key: string]: any }): void { + if (this.tempSendNotesIntoBlackHole) { this.logError(message) } else { this.notes.push({ @@ -137,12 +137,11 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE key: message, args: params, }, - trackingId: trackingId, }) } } - notifyUserWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { - if (this.blackHoleNotes) { + notifyUserWarning(message: string, params?: { [key: string]: any }): void { + if (this.tempSendNotesIntoBlackHole) { this.logWarning(message) } else { this.notes.push({ @@ -151,7 +150,6 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE key: message, args: params, }, - trackingId: trackingId, }) } } diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index 33241fed56..774aa3c9d3 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -62,11 +62,13 @@ import { CacheForRundownPlaylist, ReadOnlyCacheForRundownPlaylist } from '../../ import { BlueprintId } from '../../../../lib/collections/Blueprints' export interface ContextInfo { + /** Short name for the context (eg the blueprint function being called) */ name: string + /** Full identifier info for the context. Should be able to identify the rundown/studio/blueprint etc being executed */ identifier: string } export interface UserContextInfo extends ContextInfo { - blackHoleUserNotes?: boolean // TODO-CONTEXT remove this + tempSendUserNotesIntoBlackHole?: boolean // TODO-CONTEXT remove this } /** Common */ @@ -111,10 +113,6 @@ export class CommonContext implements ICommonContext { } } -export interface RawNote extends INoteBase { - trackingId: string | undefined -} - const studioBlueprintConfigCache: { [studioId: string]: Cache } = {} const showStyleBlueprintConfigCache: { [showStyleBaseId: string]: { [showStyleVariantId: string]: Cache } } = {} interface Cache { @@ -178,15 +176,15 @@ export class StudioContext extends CommonContext implements IStudioContext { export class StudioUserContext extends StudioContext implements IStudioUserContext { public readonly notes: INoteBase[] = [] - private readonly blackHoleNotes: boolean + private readonly tempSendNotesIntoBlackHole: boolean constructor(contextInfo: UserContextInfo, studio: Studio) { super(contextInfo, studio) - this.blackHoleNotes = contextInfo.blackHoleUserNotes ?? false + this.tempSendNotesIntoBlackHole = contextInfo.tempSendUserNotesIntoBlackHole ?? false } notifyUserError(message: string, params?: { [key: string]: any }): void { - if (this.blackHoleNotes) { + if (this.tempSendNotesIntoBlackHole) { this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { this.notes.push({ @@ -199,7 +197,7 @@ export class StudioUserContext extends StudioContext implements IStudioUserConte } } notifyUserWarning(message: string, params?: { [key: string]: any }): void { - if (this.blackHoleNotes) { + if (this.tempSendNotesIntoBlackHole) { this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { this.notes.push({ @@ -296,7 +294,7 @@ export class ShowStyleContext extends StudioContext implements IShowStyleContext export class ShowStyleUserContext extends ShowStyleContext implements IUserNotesContext { public readonly notes: INoteBase[] = [] - private readonly blackHoleNotes: boolean + private readonly tempSendNotesIntoBlackHole: boolean constructor( contextInfo: UserContextInfo, @@ -310,7 +308,7 @@ export class ShowStyleUserContext extends ShowStyleContext implements IUserNotes } notifyUserError(message: string, params?: { [key: string]: any }): void { - if (this.blackHoleNotes) { + if (this.tempSendNotesIntoBlackHole) { this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { this.notes.push({ @@ -323,7 +321,7 @@ export class ShowStyleUserContext extends ShowStyleContext implements IUserNotes } } notifyUserWarning(message: string, params?: { [key: string]: any }): void { - if (this.blackHoleNotes) { + if (this.tempSendNotesIntoBlackHole) { this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { this.notes.push({ @@ -379,31 +377,35 @@ export class RundownEventContext extends RundownContext implements IEventContext } } +export interface RawPartNote extends INoteBase { + partExternalId: string | undefined +} + export class SegmentUserContext extends RundownContext implements ISegmentUserContext { - public readonly notes: RawNote[] = [] + public readonly notes: RawPartNote[] = [] constructor(contextInfo: ContextInfo, rundown: Rundown, cache: CacheForRundownPlaylist) { super(contextInfo, rundown, cache) } - notifyUserError(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserError(message: string, params?: { [key: string]: any }, partExternalId?: string): void { this.notes.push({ type: NoteType.ERROR, message: { key: message, args: params, }, - trackingId: trackingId, + partExternalId: partExternalId, }) } - notifyUserWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { + notifyUserWarning(message: string, params?: { [key: string]: any }, partExternalId?: string): void { this.notes.push({ type: NoteType.WARNING, message: { key: message, args: params, }, - trackingId: trackingId, + partExternalId: partExternalId, }) } } @@ -449,8 +451,8 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve readonly currentPartInstance: Readonly | undefined readonly nextPartInstance: Readonly | undefined - public readonly notes: RawNote[] = [] - private readonly blackHoleNotes: boolean + public readonly notes: INoteBase[] = [] + private readonly tempSendNotesIntoBlackHole: boolean constructor( contextInfo: UserContextInfo, @@ -464,15 +466,15 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve this.currentPartInstance = currentPartInstance ? unprotectPartInstance(currentPartInstance) : undefined this.nextPartInstance = nextPartInstance ? unprotectPartInstance(nextPartInstance) : undefined - this.blackHoleNotes = contextInfo.blackHoleUserNotes ?? false + this.tempSendNotesIntoBlackHole = contextInfo.tempSendUserNotesIntoBlackHole ?? false } getCurrentTime(): number { return getCurrentTime() } - notifyUserError(message: string, params?: { [key: string]: any }, trackingId?: string): void { - if (this.blackHoleNotes) { + notifyUserError(message: string, params?: { [key: string]: any }): void { + if (this.tempSendNotesIntoBlackHole) { this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { this.notes.push({ @@ -481,12 +483,11 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve key: message, args: params, }, - trackingId: trackingId, }) } } - notifyUserWarning(message: string, params?: { [key: string]: any }, trackingId?: string): void { - if (this.blackHoleNotes) { + notifyUserWarning(message: string, params?: { [key: string]: any }): void { + if (this.tempSendNotesIntoBlackHole) { this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) } else { this.notes.push({ @@ -495,7 +496,6 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve key: message, args: params, }, - trackingId: trackingId, }) } } diff --git a/meteor/server/api/ingest/bucketAdlibs.ts b/meteor/server/api/ingest/bucketAdlibs.ts index ab6052f174..482a5920af 100644 --- a/meteor/server/api/ingest/bucketAdlibs.ts +++ b/meteor/server/api/ingest/bucketAdlibs.ts @@ -27,7 +27,7 @@ export function updateBucketAdlibFromIngestData( { name: `Bucket Ad-Lib`, identifier: `studioId=${studio._id},showStyleBaseId=${showStyle._id},showStyleVariantId=${showStyle.showStyleVariantId}`, - blackHoleUserNotes: true, // TODO-CONTEXT + tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT }, studio, undefined, diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index ed35db3b55..6782eeaae6 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -456,7 +456,7 @@ function updateRundownFromIngestData( { name: 'selectShowStyleVariant', identifier: `studioId=${studio._id},rundownId=${existingDbRundown?._id},ingestRundownId=${ingestRundown.externalId}`, - blackHoleUserNotes: true, + tempSendUserNotesIntoBlackHole: true, }, studio ) @@ -615,7 +615,7 @@ function updateRundownFromIngestData( { name: 'getRundownPlaylistInfo', identifier: `studioId=${studio._id},rundownId=${existingDbRundown?._id},ingestRundownId=${ingestRundown.externalId}`, - blackHoleUserNotes: true, + tempSendUserNotesIntoBlackHole: true, }, studio ) @@ -1450,11 +1450,11 @@ function generateSegmentContents( const blueprintRes = blueprint.getSegment(context, ingestSegment) // Ensure all parts have a valid externalId set on them - const knownPartIds = blueprintRes.parts.map((p) => p.part.externalId) + const knownPartExternalIds = blueprintRes.parts.map((p) => p.part.externalId) const segmentNotes: SegmentNote[] = [] for (const note of context.notes) { - if (!note.trackingId || knownPartIds.indexOf(note.trackingId) === -1) { + if (!note.partExternalId || knownPartExternalIds.indexOf(note.partExternalId) === -1) { segmentNotes.push( literal({ type: note.type, @@ -1492,7 +1492,7 @@ function generateSegmentContents( const notes: PartNote[] = [] for (const note of context.notes) { - if (note.trackingId === blueprintPart.part.externalId) { + if (note.partExternalId === blueprintPart.part.externalId) { notes.push( literal({ type: note.type, diff --git a/meteor/server/api/playout/playout.ts b/meteor/server/api/playout/playout.ts index 81b04c6c0f..a1a87587bc 100644 --- a/meteor/server/api/playout/playout.ts +++ b/meteor/server/api/playout/playout.ts @@ -1214,7 +1214,7 @@ export namespace ServerPlayoutAPI { identifier: `playlist=${playlist._id},rundown=${rundown._id},currentPartInstance=${ currentPartInstance._id },execution=${getRandomId()}`, - blackHoleUserNotes: true, // TODO-CONTEXT store these notes + tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT store these notes }, cache, studio, diff --git a/meteor/server/api/playout/timeline.ts b/meteor/server/api/playout/timeline.ts index fd1b33fc43..4ccc1b3a6a 100644 --- a/meteor/server/api/playout/timeline.ts +++ b/meteor/server/api/playout/timeline.ts @@ -266,7 +266,7 @@ function getTimelineRundown(cache: CacheForRundownPlaylist, studio: Studio): Tim { name: `onTimelineGenerate=${activeRundown.name}`, identifier: `blueprintId=${showStyleBlueprint0.blueprintId},rundownId=${activeRundown._id},currentPartInstanceId=${currentPartInstance?._id},nextPartInstanceId=${nextPartInstance?._id}`, - blackHoleUserNotes: true, // TODO-CONTEXT store/show these notes + tempSendUserNotesIntoBlackHole: true, // TODO-CONTEXT store/show these notes }, activeRundown, cache, From 5cdf1e5a7f722c31411c8d95efd6e2228c6e4344 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 12 Jan 2021 18:38:38 +0100 Subject: [PATCH 26/58] fix: settle inconsistencies between outdated translation code and newer blueprint changes Co-authored-by: Julian Waller --- meteor/lib/collections/TranslationsBundles.ts | 2 +- .../__tests__/context-adlibActions.test.ts | 2 +- .../api/blueprints/__tests__/context.test.ts | 4 +- .../blueprints/__tests__/postProcess.test.ts | 6 +- meteor/server/api/blueprints/config.ts | 1 + .../api/blueprints/context/adlibActions.ts | 34 +--------- .../server/api/blueprints/context/context.ts | 52 +++++++-------- .../context/syncIngestUpdateToPartInstance.ts | 38 +++++++++-- meteor/server/api/blueprints/postProcess.ts | 10 +-- meteor/server/api/ingest/bucketAdlibs.ts | 11 +--- meteor/server/api/ingest/rundownInput.ts | 30 ++++----- meteor/server/api/rundown.ts | 4 +- packages/blueprints-integration/src/api.ts | 65 +++++++++++-------- .../blueprints-integration/src/context.ts | 52 ++++++++++----- packages/blueprints-integration/src/index.ts | 1 + .../src/translations.ts | 25 +++++++ 16 files changed, 189 insertions(+), 148 deletions(-) create mode 100644 packages/blueprints-integration/src/translations.ts diff --git a/meteor/lib/collections/TranslationsBundles.ts b/meteor/lib/collections/TranslationsBundles.ts index 8da351af00..dd96037266 100644 --- a/meteor/lib/collections/TranslationsBundles.ts +++ b/meteor/lib/collections/TranslationsBundles.ts @@ -1,7 +1,7 @@ import { TransformedCollection } from '../typings/meteor' import { registerCollection, ProtectedString } from '../lib' -import { TranslationsBundle as BlueprintTranslationsBundle } from 'tv-automation-sofie-blueprints-integration' +import { TranslationsBundle as BlueprintTranslationsBundle } from '@sofie-automation/blueprints-integration' import { createMongoCollection } from './lib' /** A string identifying a translations bundle */ diff --git a/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts b/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts index 90b8e05281..fcfdff77f3 100644 --- a/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts +++ b/meteor/server/api/blueprints/__tests__/context-adlibActions.test.ts @@ -7,7 +7,7 @@ import { import { protectString, unprotectString, waitForPromise, getRandomId, getCurrentTime } from '../../../../lib/lib' import { Studio, Studios } from '../../../../lib/collections/Studios' import { IBlueprintPart, IBlueprintPiece, PieceLifespan } from '@sofie-automation/blueprints-integration' -import { NotesContext, ActionExecutionContext, ActionPartChange } from '../context' +import { ActionExecutionContext, ActionPartChange } from '../context' import { Rundown, Rundowns } from '../../../../lib/collections/Rundowns' import { PartInstance, PartInstanceId, PartInstances } from '../../../../lib/collections/PartInstances' import { diff --git a/meteor/server/api/blueprints/__tests__/context.test.ts b/meteor/server/api/blueprints/__tests__/context.test.ts index dbcac2def1..ad05da0dbe 100644 --- a/meteor/server/api/blueprints/__tests__/context.test.ts +++ b/meteor/server/api/blueprints/__tests__/context.test.ts @@ -9,7 +9,6 @@ import { getHash, protectString, unprotectObject, unprotectString, waitForPromis import { Studio } from '../../../../lib/collections/Studios' import { LookaheadMode, - NotesContext as INotesContext, IBlueprintAsRunLogEventContent, IBlueprintSegmentDB, TSR, @@ -22,10 +21,8 @@ import { } from '@sofie-automation/blueprints-integration' import { CommonContext, - StudioConfigContext, StudioContext, ShowStyleContext, - NotesContext, PartEventContext, AsRunEventContext, TimelineEventContext, @@ -42,6 +39,7 @@ import { PartInstances, unprotectPartInstance, PartInstanceId, + PartInstance, } from '../../../../lib/collections/PartInstances' import { PieceInstances, PieceInstanceInfiniteId } from '../../../../lib/collections/PieceInstances' import { SegmentId } from '../../../../lib/collections/Segments' diff --git a/meteor/server/api/blueprints/__tests__/postProcess.test.ts b/meteor/server/api/blueprints/__tests__/postProcess.test.ts index 006890c622..b272fe7c2e 100644 --- a/meteor/server/api/blueprints/__tests__/postProcess.test.ts +++ b/meteor/server/api/blueprints/__tests__/postProcess.test.ts @@ -114,7 +114,7 @@ describe('Test blueprint post-process', () => { const blueprintId = protectString('blueprint0') // Ensure that an empty array works ok - const res = postProcessStudioBaselineObjects(context, blueprintId, []) + const res = postProcessStudioBaselineObjects(studio, []) expect(res).toHaveLength(0) }) @@ -160,7 +160,7 @@ describe('Test blueprint post-process', () => { // TODO - mock getHash? - const res = postProcessStudioBaselineObjects(context, blueprintId, _.clone(rawObjects)) + const res = postProcessStudioBaselineObjects(studio, _.clone(rawObjects)) // Nothing should have been overridden (yet) _.each(rawObjects, (obj) => { @@ -218,7 +218,7 @@ describe('Test blueprint post-process', () => { ]) try { - postProcessStudioBaselineObjects(context, blueprintId, _.clone(rawObjects)) + postProcessStudioBaselineObjects(studio, _.clone(rawObjects)) fail('expected to throw') } catch (e) { expect(e.message).toBe( diff --git a/meteor/server/api/blueprints/config.ts b/meteor/server/api/blueprints/config.ts index 1a3ab55923..2f1eca1345 100644 --- a/meteor/server/api/blueprints/config.ts +++ b/meteor/server/api/blueprints/config.ts @@ -23,6 +23,7 @@ import { logger } from '../../../lib/logging' import { loadStudioBlueprint, loadShowStyleBlueprint } from './cache' import { ShowStyleBase, ShowStyleBases, ShowStyleBaseId } from '../../../lib/collections/ShowStyleBases' import { BlueprintId, Blueprints } from '../../../lib/collections/Blueprints' +import { CommonContext } from './context' /** * This whole ConfigRef logic will need revisiting for a multi-studio context, to ensure that there are strict boundaries across who can give to access to what. diff --git a/meteor/server/api/blueprints/context/adlibActions.ts b/meteor/server/api/blueprints/context/adlibActions.ts index 77d18854a6..93eb094167 100644 --- a/meteor/server/api/blueprints/context/adlibActions.ts +++ b/meteor/server/api/blueprints/context/adlibActions.ts @@ -30,12 +30,13 @@ import { PartInstanceId, PartInstance } from '../../../../lib/collections/PartIn import { CacheForRundownPlaylist } from '../../../DatabaseCaches' import { getResolvedPieces, setupPieceInstanceInfiniteProperties } from '../../playout/pieces' import { postProcessPieces, postProcessTimelineObjects } from '../postProcess' -import { NotesContext, ShowStyleContext } from './context' +import { ShowStyleContext, ShowStyleUserContext, UserContextInfo } from './context' import { isTooCloseToAutonext } from '../../playout/lib' import { ServerPlayoutAdLibAPI } from '../../playout/adlib' import { MongoQuery } from '../../../../lib/typings/meteor' import { clone } from '../../../../lib/lib' import { IBlueprintPieceSampleKeys, IBlueprintMutatablePartSampleKeys } from './lib' +import { NoteType } from '../../../../lib/api/notes' export enum ActionPartChange { NONE = 0, @@ -43,7 +44,7 @@ export enum ActionPartChange { } /** Actions */ -export class ActionExecutionContext extends ShowStyleContext implements IActionExecutionContext, IEventContext { +export class ActionExecutionContext extends ShowStyleUserContext implements IActionExecutionContext, IEventContext { private readonly _cache: CacheForRundownPlaylist private readonly rundownPlaylist: RundownPlaylist private readonly rundown: Rundown @@ -66,35 +67,6 @@ export class ActionExecutionContext extends ShowStyleContext implements IActionE this.rundownPlaylist = rundownPlaylist this.rundown = rundown this.takeAfterExecute = false - - this.tempSendNotesIntoBlackHole = contextInfo.tempSendUserNotesIntoBlackHole ?? false - } - - notifyUserError(message: string, params?: { [key: string]: any }): void { - if (this.tempSendNotesIntoBlackHole) { - this.logError(message) - } else { - this.notes.push({ - type: NoteType.ERROR, - message: { - key: message, - args: params, - }, - }) - } - } - notifyUserWarning(message: string, params?: { [key: string]: any }): void { - if (this.tempSendNotesIntoBlackHole) { - this.logWarning(message) - } else { - this.notes.push({ - type: NoteType.WARNING, - message: { - key: message, - args: params, - }, - }) - } } private _getPartInstanceId(part: 'current' | 'next'): PartInstanceId | null { diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index 785b143eae..d06fa6f222 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -21,15 +21,7 @@ import { check, Match } from '../../../../lib/check' import { logger } from '../../../../lib/logging' import { ICommonContext, - NotesContext as INotesContext, - ShowStyleContext as IShowStyleContext, - RundownContext as IRundownContext, - SegmentContext as ISegmentContext, - EventContext as IEventContext, - AsRunEventContext as IAsRunEventContext, - PartEventContext as IPartEventContext, - TimelineEventContext as ITimelineEventContext, - IStudioConfigContext, + IUserNotesContext, IStudioContext, IStudioUserContext, BlueprintMappings, @@ -43,6 +35,13 @@ import { IBlueprintExternalMessageQueueObj, ExtendedIngestRundown, OnGenerateTimelineObj, + IShowStyleContext, + IRundownContext, + IEventContext, + ISegmentUserContext, + IPartEventContext, + ITimelineEventContext, + IAsRunEventContext, } from '@sofie-automation/blueprints-integration' import { Studio, StudioId } from '../../../../lib/collections/Studios' import { @@ -71,6 +70,18 @@ import { CacheForRundownPlaylist, ReadOnlyCacheForRundownPlaylist } from '../../ import { DeepReadonly } from 'utility-types' import { Random } from 'meteor/random' import { OnGenerateTimelineObjExt } from '../../../../lib/collections/Timeline' +import { BlueprintId } from '../../../../lib/collections/Blueprints' +import _ from 'underscore' + +export interface ContextInfo { + /** Short name for the context (eg the blueprint function being called) */ + name: string + /** Full identifier info for the context. Should be able to identify the rundown/studio/blueprint etc being executed */ + identifier: string +} +export interface UserContextInfo extends ContextInfo { + tempSendUserNotesIntoBlackHole?: boolean // TODO-CONTEXT remove this +} /** Common */ @@ -183,7 +194,6 @@ export class StudioUserContext extends StudioContext implements IStudioUserConte } /** Show Style Variant */ - export class ShowStyleContext extends StudioContext implements IShowStyleContext { constructor( contextInfo: ContextInfo, @@ -195,8 +205,6 @@ export class ShowStyleContext extends StudioContext implements IShowStyleContext ) { super(contextInfo, studio) } - error: (message: string) => void - warning: (message: string) => void getShowStyleBase(): ShowStyleBase { if (this.cache && this._rundown) { @@ -295,8 +303,6 @@ export class RundownContext extends ShowStyleContext implements IRundownContext this._rundown = rundown this.playlistId = rundown.playlistId } - error: (message: string) => void - warning: (message: string) => void } export class RundownEventContext extends RundownContext implements IEventContext { @@ -380,8 +386,6 @@ export class PartEventContext extends RundownContext implements IPartEventContex this.part = unprotectPartInstance(partInstance) } - error: (message: string) => void - warning: (message: string) => void getCurrentTime(): number { return getCurrentTime() @@ -405,7 +409,6 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve } constructor( - contextInfo: UserContextInfo, rundown: Rundown, cache: CacheForRundownPlaylist, previousPartInstance: PartInstance | undefined, @@ -413,13 +416,12 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve nextPartInstance: PartInstance | undefined ) { super( + { + name: rundown.name, + identifier: `rundownId=${rundown._id},previousPartInstance=${previousPartInstance?._id},currentPartInstance=${currentPartInstance?._id},nextPartInstance=${nextPartInstance?._id}`, + }, rundown, - cache, - new NotesContext( - rundown.name, - `rundownId=${rundown._id},previousPartInstance=${previousPartInstance?._id},currentPartInstance=${currentPartInstance?._id},nextPartInstance=${nextPartInstance?._id}`, - false - ) + cache ) this.currentPartInstance = currentPartInstance ? unprotectPartInstance(currentPartInstance) : undefined @@ -430,8 +432,6 @@ export class TimelineEventContext extends RundownContext implements ITimelineEve this._knownSessions = clone(cache.RundownPlaylists.findOne(cache.containsDataFromPlaylist)?.trackedAbSessions) ?? [] } - error: (message: string) => void - warning: (message: string) => void getCurrentTime(): number { return getCurrentTime() @@ -587,8 +587,6 @@ export class AsRunEventContext extends RundownContext implements IAsRunEventCont super(contextInfo, rundown, cache) this.asRunEvent = unprotectObject(asRunEvent) } - error: (message: string) => void - warning: (message: string) => void getCurrentTime(): number { return getCurrentTime() diff --git a/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts b/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts index 317e73e481..d8474dbc54 100644 --- a/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts +++ b/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts @@ -1,5 +1,5 @@ import { clone } from 'underscore' -import { RundownContext, NotesContext } from './context' +import { ContextInfo, RundownContext } from './context' import { CacheForRundownPlaylist, ReadOnlyCacheForRundownPlaylist } from '../../../DatabaseCaches' import { IBlueprintPiece, @@ -7,7 +7,7 @@ import { OmitId, IBlueprintMutatablePart, IBlueprintPartInstance, - SyncIngestUpdateToPartInstanceContext as ISyncIngestUpdateToPartInstanceContext, + ISyncIngestUpdateToPartInstanceContext, } from '@sofie-automation/blueprints-integration' import { PartInstance, DBPartInstance, PartInstances } from '../../../../lib/collections/PartInstances' import _ from 'underscore' @@ -31,23 +31,26 @@ import { Rundown } from '../../../../lib/collections/Rundowns' import { DbCacheWriteCollection } from '../../../DatabaseCache' import { setupPieceInstanceInfiniteProperties } from '../../playout/pieces' import { Meteor } from 'meteor/meteor' +import { INoteBase, NoteType } from '../../../../lib/api/notes' export class SyncIngestUpdateToPartInstanceContext extends RundownContext implements ISyncIngestUpdateToPartInstanceContext { private readonly _partInstanceCache: DbCacheWriteCollection private readonly _pieceInstanceCache: DbCacheWriteCollection private readonly _proposedPieceInstances: Map + public readonly notes: INoteBase[] = [] + private readonly tempSendNotesIntoBlackHole: boolean constructor( + contextInfo: ContextInfo, rundown: Rundown, cache: ReadOnlyCacheForRundownPlaylist, - notesContext: NotesContext, private partInstance: PartInstance, pieceInstances: PieceInstance[], proposedPieceInstances: PieceInstance[], private playStatus: 'current' | 'next' ) { - super(rundown, cache, notesContext) + super(contextInfo, rundown, cache) // Create temporary cache databases this._pieceInstanceCache = new DbCacheWriteCollection(PieceInstances) @@ -59,6 +62,33 @@ export class SyncIngestUpdateToPartInstanceContext extends RundownContext this._proposedPieceInstances = normalizeArrayToMap(proposedPieceInstances, '_id') } + notifyUserError(message: string, params?: { [key: string]: any }): void { + if (this.tempSendNotesIntoBlackHole) { + this.logError(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.ERROR, + message: { + key: message, + args: params, + }, + }) + } + } + notifyUserWarning(message: string, params?: { [key: string]: any }): void { + if (this.tempSendNotesIntoBlackHole) { + this.logWarning(`UserNotes: "${message}", ${JSON.stringify(params)}`) + } else { + this.notes.push({ + type: NoteType.WARNING, + message: { + key: message, + args: params, + }, + }) + } + } + applyChangesToCache(cache: CacheForRundownPlaylist) { this._pieceInstanceCache.updateOtherCacheWithData(cache.PieceInstances) this._partInstanceCache.updateOtherCacheWithData(cache.PartInstances) diff --git a/meteor/server/api/blueprints/postProcess.ts b/meteor/server/api/blueprints/postProcess.ts index 83c8d71d92..e21daed6ac 100644 --- a/meteor/server/api/blueprints/postProcess.ts +++ b/meteor/server/api/blueprints/postProcess.ts @@ -10,7 +10,8 @@ import { IBlueprintAdLibPiece, TSR, IBlueprintActionManifest, - NotesContext as INotesContext, + ICommonContext, + IShowStyleContext, } from '@sofie-automation/blueprints-integration' import { RundownAPI } from '../../../lib/api/rundown' import { BucketAdLib } from '../../../lib/collections/BucketAdlibs' @@ -25,6 +26,7 @@ import { prefixAllObjectIds } from '../playout/lib' import { SegmentId } from '../../../lib/collections/Segments' import { profiler } from '../profiler' import { BucketAdLibAction } from '../../../lib/collections/BucketAdlibActions' +import { CommonContext, ShowStyleContext } from './context' /** * @@ -32,7 +34,7 @@ import { BucketAdLibAction } from '../../../lib/collections/BucketAdlibActions' * prefixAllTimelineObjects: Add a prefix to the timeline object ids, to ensure duplicate ids don't occur when inserting a copy of a piece */ export function postProcessPieces( - innerContext: ShowStyleContext, + innerContext: IShowStyleContext, pieces: IBlueprintPiece[], blueprintId: BlueprintId, rundownId: RundownId, @@ -166,7 +168,7 @@ export function postProcessAdLibPieces( _id: protectString( innerContext.getHashId(`${blueprintId}_${partId}_adlib_piece_${orgAdlib.externalId}_${i}`) ), - rundownId: protectString(innerContext.rundown._id), + rundownId: rundownId, partId: partId, status: RundownAPI.PieceStatusCode.UNKNOWN, } @@ -233,7 +235,7 @@ export function postProcessAdLibActions( } export function postProcessStudioBaselineObjects(studio: Studio, objs: TSR.TSRTimelineObjBase[]): TimelineObjRundown[] { - const context = new NotesContext('studio', 'studio', false) + const context = new CommonContext({ identifier: 'studio', name: 'studio' }) return postProcessTimelineObjects(context, protectString('studio'), studio.blueprintId!, objs, false) } diff --git a/meteor/server/api/ingest/bucketAdlibs.ts b/meteor/server/api/ingest/bucketAdlibs.ts index 3ff8d1aab5..97e61250dc 100644 --- a/meteor/server/api/ingest/bucketAdlibs.ts +++ b/meteor/server/api/ingest/bucketAdlibs.ts @@ -3,13 +3,11 @@ import { IBlueprintActionManifest, IBlueprintAdLibPiece, IngestAdlib } from '@so import { ShowStyleCompound } from '../../../lib/collections/ShowStyleVariants' import { Studio } from '../../../lib/collections/Studios' import { loadShowStyleBlueprint } from '../blueprints/cache' -import { ShowStyleContext, NotesContext } from '../blueprints/context' import { postProcessBucketAction, postProcessBucketAdLib } from '../blueprints/postProcess' import { RundownImportVersions } from '../../../lib/collections/Rundowns' import { PackageInfo } from '../../coreSystem' import { BucketAdLibs } from '../../../lib/collections/BucketAdlibs' import { BucketId } from '../../../lib/collections/Buckets' -import { PieceId } from '../../../lib/collections/Pieces' import { cleanUpExpectedMediaItemForBucketAdLibActions, cleanUpExpectedMediaItemForBucketAdLibPiece, @@ -17,14 +15,9 @@ import { updateExpectedMediaItemForBucketAdLibPiece, } from '../expectedMediaItems' import { BucketAdLibActions } from '../../../lib/collections/BucketAdlibActions' -import { - asyncCollectionFindFetch, - asyncCollectionFindOne, - asyncCollectionRemove, - waitForPromiseAll, -} from '../../../lib/lib' -import { syncFunction } from '../../codeControl' +import { asyncCollectionFindFetch, asyncCollectionRemove, waitForPromiseAll } from '../../../lib/lib' import { bucketSyncFunction } from '../buckets' +import { ShowStyleUserContext } from '../blueprints/context' function isAdlibAction(adlib: IBlueprintActionManifest | IBlueprintAdLibPiece): adlib is IBlueprintActionManifest { return !!(adlib as IBlueprintActionManifest).actionId diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index 6785b61715..f32240fafa 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -50,9 +50,11 @@ import { loadShowStyleBlueprint, WrappedShowStyleBlueprint } from '../blueprints import { ShowStyleContext, RundownContext, - SegmentContext, - NotesContext, SyncIngestUpdateToPartInstanceContext, + StudioUserContext, + ShowStyleUserContext, + CommonContext, + SegmentUserContext, } from '../blueprints/context' import { Blueprints, Blueprint, BlueprintId } from '../../../lib/collections/Blueprints' import { @@ -638,7 +640,7 @@ function updateRundownFromIngestData( } } - const rundownPlaylistInfo = produceRundownPlaylistInfoFromRundown(studio, dbRundownData, peripheralDevice) + const rundownPlaylistInfo = produceRundownPlaylistInfoFromRundown(dbRundownData, peripheralDevice) dbRundownData.playlistId = rundownPlaylistInfo.rundownPlaylist._id // Save rundown into database: @@ -1237,17 +1239,14 @@ function updateSegmentFromIngestData( ingestSegment.parts = _.sortBy(ingestSegment.parts, (s) => s.rank) - const notesContext = new NotesContext(ingestSegment.name, `rundownId=${rundown._id},segmentId=${segmentId}`, true) - const context = new SegmentContext(rundown, cache, notesContext) - const blueprintSegment = blueprint.blueprint.getSegment(context, ingestSegment) - const { parts, segmentPieces, adlibPieces, adlibActions, newSegment } = generateSegmentContents( - context, + cache, + rundown, + blueprint.blueprint, blueprint.blueprintId, ingestSegment, existingSegment, - existingParts, - blueprintSegment + existingParts ) const prepareSaveParts = prepareSaveIntoCache( @@ -1437,13 +1436,12 @@ function syncChangesToPartInstances( } const syncContext = new SyncIngestUpdateToPartInstanceContext( + { + name: `Update to ${newPart.externalId}`, + identifier: `rundownId=${newPart.rundownId},segmentId=${newPart.segmentId}`, + }, rundown, cache, - new NotesContext( - `Update to ${newPart.externalId}`, - `rundownId=${newPart.rundownId},segmentId=${newPart.segmentId}`, - true - ), existingPartInstance, pieceInstancesInPart, proposedPieceInstances, @@ -1469,7 +1467,7 @@ function syncChangesToPartInstances( if (!existingPartInstance.part.notes) existingPartInstance.part.notes = [] const notes: PartNote[] = existingPartInstance.part.notes let changed = false - for (const note of syncContext.notesContext.getNotes()) { + for (const note of syncContext.notes) { changed = true notes.push( literal({ diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index cc77dacdd8..a16aebe0f9 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -40,7 +40,6 @@ import { ExtendedIngestRundown, BlueprintResultRundownPlaylist, } from '@sofie-automation/blueprints-integration' -import { StudioConfigContext } from './blueprints/context' import { loadStudioBlueprint, loadShowStyleBlueprint } from './blueprints/cache' import { PackageInfo } from '../coreSystem' import { IngestActions } from './ingest/actions' @@ -76,7 +75,7 @@ import { Mongo } from 'meteor/mongo' import { getPlaylistIdFromExternalId, removeEmptyPlaylists } from './rundownPlaylist' export function selectShowStyleVariant( - context: StudioUserContext, + context: IStudioUserContext, ingestRundown: ExtendedIngestRundown ): { variant: ShowStyleVariant; base: ShowStyleBase } | null { const studio = context.getStudio() @@ -217,7 +216,6 @@ export function produceRundownPlaylistRanks( * This function is (/can be) run before the playlist has been created. */ export function produceRundownPlaylistInfoFromRundown( - studio: Studio, currentRundown: DBRundown, peripheralDevice: PeripheralDevice | undefined ): RundownPlaylistAndOrder { diff --git a/packages/blueprints-integration/src/api.ts b/packages/blueprints-integration/src/api.ts index ffb9a92b39..8c8c4220fd 100644 --- a/packages/blueprints-integration/src/api.ts +++ b/packages/blueprints-integration/src/api.ts @@ -3,17 +3,17 @@ import { TSRTimelineObjBase } from 'timeline-state-resolver-types' import { ActionUserData, IBlueprintActionManifest } from './action' import { ConfigManifestEntry } from './config' import { - ActionExecutionContext, - SyncIngestUpdateToPartInstanceContext, - AsRunEventContext, - EventContext, - IStudioConfigContext, + IActionExecutionContext, + ISyncIngestUpdateToPartInstanceContext, + IAsRunEventContext, IStudioContext, - PartEventContext, - RundownContext, - SegmentContext, - ShowStyleContext, - TimelineEventContext, + IPartEventContext, + IRundownContext, + ITimelineEventContext, + IStudioUserContext, + ISegmentUserContext, + IShowStyleUserContext, + ICommonContext, } from './context' import { IngestAdlib, ExtendedIngestRundown, IngestSegment } from './ingest' import { IBlueprintExternalMessageQueueObj } from './message' @@ -73,21 +73,27 @@ export interface StudioBlueprintManifest extends BlueprintManifestBase { /** A list of Migration steps related to a Studio */ studioMigrations: MigrationStep[] + /** Translations connected to the studio (as stringified JSON) */ + translations?: string + /** Returns the items used to build the baseline (default state) of a studio, this is the baseline used when there's no active rundown */ getBaseline: (context: IStudioContext) => TSRTimelineObjBase[] /** Returns the id of the show style to use for a rundown, return null to ignore that rundown */ getShowStyleId: ( - context: IStudioConfigContext, + context: IStudioUserContext, showStyles: IBlueprintShowStyleBase[], ingestRundown: ExtendedIngestRundown ) => string | null /** Returns information about the playlist this rundown is a part of, return null to not make it a part of a playlist */ - getRundownPlaylistInfo?: (rundowns: IBlueprintRundownDB[]) => BlueprintResultRundownPlaylist | null + getRundownPlaylistInfo?: ( + context: IStudioUserContext, + rundowns: IBlueprintRundownDB[] + ) => BlueprintResultRundownPlaylist | null /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ - preprocessConfig?: (config: IBlueprintConfig) => unknown + preprocessConfig?: (context: ICommonContext, config: IBlueprintConfig) => unknown } export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { @@ -98,28 +104,31 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** A list of Migration steps related to a ShowStyle */ showStyleMigrations: MigrationStep[] + /** Translations connected to the studio (as stringified JSON) */ + translations?: string + // -------------------------------------------------------------- // Callbacks called by Core: /** Returns the id of the show style variant to use for a rundown, return null to ignore that rundown */ getShowStyleVariantId: ( - context: IStudioConfigContext, + context: IStudioUserContext, showStyleVariants: IBlueprintShowStyleVariant[], ingestRundown: ExtendedIngestRundown ) => string | null /** Generate rundown from ingest data. return null to ignore that rundown */ - getRundown: (context: ShowStyleContext, ingestRundown: ExtendedIngestRundown) => BlueprintResultRundown + getRundown: (context: IShowStyleUserContext, ingestRundown: ExtendedIngestRundown) => BlueprintResultRundown /** Generate segment from ingest data */ - getSegment: (context: SegmentContext, ingestSegment: IngestSegment) => BlueprintResultSegment + getSegment: (context: ISegmentUserContext, ingestSegment: IngestSegment) => BlueprintResultSegment /** * Allows the blueprint to custom-modify the PartInstance, on ingest data update (this is run after getSegment() ) * Warning: This is currently an experimental api, and is likely to break in the next release */ syncIngestUpdateToPartInstance?: ( - context: SyncIngestUpdateToPartInstanceContext, + context: ISyncIngestUpdateToPartInstanceContext, existingPartInstance: BlueprintSyncIngestPartInstance, newData: BlueprintSyncIngestNewData, playoutStatus: 'current' | 'next' @@ -127,7 +136,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Execute an action defined by an IBlueprintActionManifest */ executeAction?: ( - context: EventContext & ActionExecutionContext, + context: IActionExecutionContext, actionId: string, userData: ActionUserData, triggerMode?: string @@ -135,26 +144,26 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Generate adlib piece from ingest data */ getAdlibItem?: ( - context: ShowStyleContext, + context: IShowStyleUserContext, ingestItem: IngestAdlib ) => IBlueprintAdLibPiece | IBlueprintActionManifest | null /** Preprocess config before storing it by core to later be returned by context's getShowStyleConfig. If not provided, getShowStyleConfig will return unprocessed blueprint config */ - preprocessConfig?: (config: IBlueprintConfig) => unknown + preprocessConfig?: (context: ICommonContext, config: IBlueprintConfig) => unknown // Events - onRundownActivate?: (context: EventContext & RundownContext) => Promise - onRundownFirstTake?: (context: EventContext & PartEventContext) => Promise - onRundownDeActivate?: (context: EventContext & RundownContext) => Promise + onRundownActivate?: (context: IRundownContext) => Promise + onRundownFirstTake?: (context: IPartEventContext) => Promise + onRundownDeActivate?: (context: IRundownContext) => Promise /** Called after a Take action */ - onPreTake?: (context: EventContext & PartEventContext) => Promise - onPostTake?: (context: EventContext & PartEventContext) => Promise + onPreTake?: (context: IPartEventContext) => Promise + onPostTake?: (context: IPartEventContext) => Promise /** Called after the timeline has been generated, used to manipulate the timeline */ onTimelineGenerate?: ( - context: TimelineEventContext, + context: ITimelineEventContext, timeline: OnGenerateTimelineObj[], previousPersistentState: TimelinePersistentState | undefined, previousPartEndState: PartEndState | undefined, @@ -163,7 +172,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Called just before taking the next part. This generates some persisted data used by onTimelineGenerate to modify the timeline based on the previous part (eg, persist audio levels) */ getEndStateForPart?: ( - context: RundownContext, + context: IRundownContext, previousPersistentState: TimelinePersistentState | undefined, previousPartEndState: PartEndState | undefined, resolvedPieces: IBlueprintResolvedPieceInstance[], @@ -171,7 +180,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { ) => PartEndState /** Called after an as-run event is created */ - onAsRunEvent?: (context: EventContext & AsRunEventContext) => Promise + onAsRunEvent?: (context: IAsRunEventContext) => Promise } export type PartEndState = unknown diff --git a/packages/blueprints-integration/src/context.ts b/packages/blueprints-integration/src/context.ts index 4e698f9124..d77f78a17b 100644 --- a/packages/blueprints-integration/src/context.ts +++ b/packages/blueprints-integration/src/context.ts @@ -26,51 +26,67 @@ export interface ICommonContext { getHashId: (originString: string, originIsNotUnique?: boolean) => string /** Un-hash, is return the string that created the hash */ unhashId: (hash: string) => string + + /** Log a message to the sofie log with level 'debug' */ + logDebug: (message: string) => void + /** Log a message to the sofie log with level 'info' */ + logInfo: (message: string) => void + /** Log a message to the sofie log with level 'warn' */ + logWarning: (message: string) => void + /** Log a message to the sofie log with level 'error' */ + logError: (message: string) => void } -export interface NotesContext extends ICommonContext { - error: (message: string) => void - warning: (message: string) => void +export interface IUserNotesContext extends ICommonContext { + /** Display a notification to the user of an error */ + notifyUserError(message: string, params?: { [key: string]: any }): void + /** Display a notification to the user of an warning */ + notifyUserWarning(message: string, params?: { [key: string]: any }): void } /** Studio */ -export interface IStudioConfigContext { +export interface IStudioContext extends ICommonContext { /** Returns the Studio blueprint config. If StudioBlueprintManifest.preprocessConfig is provided, a config preprocessed by that function is returned, otherwise it is returned unprocessed */ getStudioConfig: () => unknown /** Returns a reference to a studio config value, that can later be resolved in Core */ getStudioConfigRef(configKey: string): string -} -export interface IStudioContext extends IStudioConfigContext { + /** Get the mappings for the studio */ getStudioMappings: () => Readonly } +export interface IStudioUserContext extends IUserNotesContext, IStudioContext {} + /** Show Style Variant */ -export interface IShowStyleConfigContext { +export interface IShowStyleContext extends ICommonContext, IStudioContext { /** Returns a ShowStyle blueprint config. If ShowStyleBlueprintManifest.preprocessConfig is provided, a config preprocessed by that function is returned, otherwise it is returned unprocessed */ getShowStyleConfig: () => unknown /** Returns a reference to a showStyle config value, that can later be resolved in Core */ getShowStyleConfigRef(configKey: string): string } -export interface ShowStyleContext extends NotesContext, IStudioContext, IShowStyleConfigContext {} +export interface IShowStyleUserContext extends IUserNotesContext, IShowStyleContext {} /** Rundown */ -export interface RundownContext extends ShowStyleContext { +export interface IRundownContext extends IShowStyleContext { readonly rundownId: string readonly rundown: Readonly } -export interface SegmentContext extends RundownContext { - error: (message: string, partExternalId?: string) => void - warning: (message: string, partExternalId?: string) => void +export interface IRundownUserContext extends IUserNotesContext, IRundownContext {} + +export interface ISegmentUserContext extends IUserNotesContext, IRundownContext { + /** Display a notification to the user of an error */ + notifyUserError: (message: string, params?: { [key: string]: any }, partExternalId?: string) => void + /** Display a notification to the user of an warning */ + notifyUserWarning: (message: string, params?: { [key: string]: any }, partExternalId?: string) => void } /** Actions */ -export interface ActionExecutionContext extends ShowStyleContext { +export interface IActionExecutionContext extends IShowStyleUserContext, IEventContext { /** Data fetching */ // getIngestRundown(): IngestRundown // TODO - for which part? /** Get a PartInstance which can be modified */ @@ -119,7 +135,7 @@ export interface ActionExecutionContext extends ShowStyleContext { } /** Actions */ -export interface SyncIngestUpdateToPartInstanceContext extends RundownContext { +export interface ISyncIngestUpdateToPartInstanceContext extends IRundownUserContext { /** Sync a pieceInstance. Inserts the pieceInstance if new, updates if existing. Optionally pass in a mutated Piece, to change the content of the instance */ syncPieceInstance( pieceInstanceId: string, @@ -155,11 +171,11 @@ export interface SyncIngestUpdateToPartInstanceContext extends RundownContext { /** Events */ -export interface EventContext { +export interface IEventContext { getCurrentTime(): number } -export interface TimelineEventContext extends EventContext, RundownContext { +export interface ITimelineEventContext extends IEventContext, IRundownContext { readonly currentPartInstance: Readonly | undefined readonly nextPartInstance: Readonly | undefined @@ -175,11 +191,11 @@ export interface TimelineEventContext extends EventContext, RundownContext { getTimelineObjectAbSessionId(obj: OnGenerateTimelineObj, sessionName: string): string | undefined } -export interface PartEventContext extends EventContext, RundownContext { +export interface IPartEventContext extends IEventContext, IRundownContext { readonly part: Readonly } -export interface AsRunEventContext extends RundownContext { +export interface IAsRunEventContext extends IEventContext, IRundownContext { readonly asRunEvent: Readonly formatDateAsTimecode(time: number): string diff --git a/packages/blueprints-integration/src/index.ts b/packages/blueprints-integration/src/index.ts index 58d7db8a28..c9fe596e0f 100644 --- a/packages/blueprints-integration/src/index.ts +++ b/packages/blueprints-integration/src/index.ts @@ -14,3 +14,4 @@ export * from './showStyle' export * from './studio' export * from './timeline' export * from './util' +export * from './translations' diff --git a/packages/blueprints-integration/src/translations.ts b/packages/blueprints-integration/src/translations.ts new file mode 100644 index 0000000000..bcb92a535e --- /dev/null +++ b/packages/blueprints-integration/src/translations.ts @@ -0,0 +1,25 @@ +export { TranslationsBundle, TranslationsBundleType, I18NextData } + +enum TranslationsBundleType { + /** i18next JSON data */ + I18NEXT = 'i18next', +} + +interface I18NextData { + [key: string]: string +} + +/** + * A bundle of translations + */ +interface TranslationsBundle { + type: TranslationsBundleType + /** language code (example: 'nb'), annotates what language the translations are for */ + language: string + /** optional namespace for the bundle */ + namespace?: string + /** encoding used for the data, typically utf-8 */ + encoding?: string + /** the actual translations as key/value pairs */ + data: I18NextData +} From 5908754e9c8352d3c408914de20fba69d02308b7 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 13 Jan 2021 17:41:46 +0100 Subject: [PATCH 27/58] fix: more context usage inconsistencies --- meteor/server/api/ingest/rundownInput.ts | 2 +- meteor/server/api/rundown.ts | 32 +++++++++++++++++++++--- meteor/server/api/translationsBundles.ts | 2 +- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/meteor/server/api/ingest/rundownInput.ts b/meteor/server/api/ingest/rundownInput.ts index f32240fafa..f1d468fe57 100644 --- a/meteor/server/api/ingest/rundownInput.ts +++ b/meteor/server/api/ingest/rundownInput.ts @@ -640,7 +640,7 @@ function updateRundownFromIngestData( } } - const rundownPlaylistInfo = produceRundownPlaylistInfoFromRundown(dbRundownData, peripheralDevice) + const rundownPlaylistInfo = produceRundownPlaylistInfoFromRundown(studio, dbRundownData, peripheralDevice) dbRundownData.playlistId = rundownPlaylistInfo.rundownPlaylist._id // Save rundown into database: diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index 13689a151a..c707bdf3a3 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -40,6 +40,7 @@ import { BlueprintResultOrderedRundowns, ExtendedIngestRundown, BlueprintResultRundownPlaylist, + IStudioUserContext, } from '@sofie-automation/blueprints-integration' import { loadStudioBlueprint, loadShowStyleBlueprint } from './blueprints/cache' import { PackageInfo } from '../coreSystem' @@ -74,9 +75,10 @@ import { updateRundownsInPlaylist } from './ingest/rundownInput' import { Mongo } from 'meteor/mongo' import { getPlaylistIdFromExternalId, removeEmptyPlaylists } from './rundownPlaylist' import { ExpectedMediaItems } from '../../lib/collections/ExpectedMediaItems' +import { StudioUserContext } from './blueprints/context' export function selectShowStyleVariant( - context: IStudioUserContext, + context: StudioUserContext, ingestRundown: ExtendedIngestRundown ): { variant: ShowStyleVariant; base: ShowStyleBase } | null { const studio = context.getStudio() @@ -200,7 +202,17 @@ export function produceRundownPlaylistRanks( const { rundowns } = getAllRundownsInPlaylist(existingPlaylist._id, existingPlaylist.externalId) const playlistInfo: BlueprintResultRundownPlaylist | null = studioBlueprint.blueprint.getRundownPlaylistInfo - ? studioBlueprint.blueprint.getRundownPlaylistInfo(unprotectObjectArray(rundowns)) + ? studioBlueprint.blueprint.getRundownPlaylistInfo( + new StudioUserContext( + { + name: 'produceRundownPlaylistRanks', + identifier: `studioId=${studio._id},playlistId=${unprotectString(playlistId)}`, + tempSendUserNotesIntoBlackHole: true, + }, + studio + ), + unprotectObjectArray(rundowns) + ) : null if (playlistInfo) { @@ -217,10 +229,10 @@ export function produceRundownPlaylistRanks( * This function is (/can be) run before the playlist has been created. */ export function produceRundownPlaylistInfoFromRundown( + studio: Studio, currentRundown: DBRundown, peripheralDevice: PeripheralDevice | undefined ): RundownPlaylistAndOrder { - const studio = context.getStudio() const studioBlueprint = loadStudioBlueprint(studio) if (!studioBlueprint) throw new Meteor.Error(500, `Studio "${studio._id}" does not have a blueprint`) @@ -256,7 +268,19 @@ export function produceRundownPlaylistInfoFromRundown( const rundowns = getAllRundownsInPlaylist2(playlistId, playlistExternalId) const playlistInfo: BlueprintResultRundownPlaylist | null = studioBlueprint.blueprint.getRundownPlaylistInfo - ? studioBlueprint.blueprint.getRundownPlaylistInfo(unprotectObjectArray(rundowns)) + ? studioBlueprint.blueprint.getRundownPlaylistInfo( + new StudioUserContext( + { + name: 'produceRundownPlaylistInfoFromRundown', + identifier: `studioId=${studio._id},playlistId=${unprotectString( + playlistId + )},rundownId=${currentRundown._id}`, + tempSendUserNotesIntoBlackHole: true, + }, + studio + ), + unprotectObjectArray(rundowns) + ) : null if (playlistInfo) { diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index 501a941ed7..b504e658a4 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -1,5 +1,5 @@ import { TranslationsBundles, TranslationsBundleId } from '../../lib/collections/TranslationsBundles' -import { TranslationsBundle, TranslationsBundleType } from 'tv-automation-sofie-blueprints-integration' +import { TranslationsBundle, TranslationsBundleType } from '@sofie-automation/blueprints-integration' import { getRandomId, unprotectString } from '../../lib/lib' import { logger } from '../logging' import { BlueprintId } from '../../lib/collections/Blueprints' From 153a322b0bd5c8de22d2e5965c4f7d8aee41a075 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 20 Jan 2021 14:58:30 +0100 Subject: [PATCH 28/58] chore: fix failing test --- meteor/server/api/blueprints/__tests__/postProcess.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/server/api/blueprints/__tests__/postProcess.test.ts b/meteor/server/api/blueprints/__tests__/postProcess.test.ts index 34db52598e..d3ec3c3ade 100644 --- a/meteor/server/api/blueprints/__tests__/postProcess.test.ts +++ b/meteor/server/api/blueprints/__tests__/postProcess.test.ts @@ -2,7 +2,7 @@ import * as _ from 'underscore' import { setupDefaultStudioEnvironment, DefaultEnvironment } from '../../../../__mocks__/helpers/database' import { Rundown } from '../../../../lib/collections/Rundowns' import { testInFiber } from '../../../../__mocks__/helpers/jest' -import { literal, protectString, waitForPromise } from '../../../../lib/lib' +import { literal, protectString, unprotectString, waitForPromise } from '../../../../lib/lib' import { Studios, Studio } from '../../../../lib/collections/Studios' import { postProcessStudioBaselineObjects, @@ -181,7 +181,7 @@ describe('Test blueprint post-process', () => { testInFiber('duplicate ids', () => { const studio = getStudio() const context = getStudioContext(studio) - const blueprintId = protectString('blueprint0') + const blueprintId = protectString(unprotectString(studio.blueprintId)) // the unit could modify the value, so make a literal copy const rawObjects = literal([ { From 088dd4c6d53712fa53b971394751e4c33beda124 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 20 Jan 2021 16:22:22 +0100 Subject: [PATCH 29/58] chore: [publish] From 948622c1231565aa8e4e41dcd667abaf1762a720 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Thu, 21 Jan 2021 11:43:25 +0100 Subject: [PATCH 30/58] chore: ignore .vscode/tasks.json --- .vscode/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .vscode/.gitignore diff --git a/.vscode/.gitignore b/.vscode/.gitignore new file mode 100644 index 0000000000..7fbb44b7a0 --- /dev/null +++ b/.vscode/.gitignore @@ -0,0 +1 @@ +tasks.json \ No newline at end of file From a3284a8f217f4040b79cb49a95dccb969e12bb82 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Fri, 22 Jan 2021 18:07:27 +0100 Subject: [PATCH 31/58] feat: add interface IAsRunEventUserContext --- packages/blueprints-integration/src/context.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/blueprints-integration/src/context.ts b/packages/blueprints-integration/src/context.ts index a93f198601..569625170d 100644 --- a/packages/blueprints-integration/src/context.ts +++ b/packages/blueprints-integration/src/context.ts @@ -251,3 +251,5 @@ export interface IAsRunEventContext extends IEventContext, IRundownContext { /** Get the ingest data related to a partInstance */ getIngestDataForPartInstance(partInstance: Readonly): Readonly | undefined } + +export interface IAsRunEventUserContext extends IAsRunEventContext, IUserNotesContext {} From baf15eef525cefaebb29d7673485eb5c21acf848 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Fri, 22 Jan 2021 18:07:51 +0100 Subject: [PATCH 32/58] chore: [publish] From 5738cf97b70c452ecfc3ed0b5a3515105b292d38 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 12:48:30 +0100 Subject: [PATCH 33/58] feat: ShowStyleBlueprintManifest.onAsRunEvent context changed to user context --- packages/blueprints-integration/src/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blueprints-integration/src/api.ts b/packages/blueprints-integration/src/api.ts index 8c8c4220fd..72326dd481 100644 --- a/packages/blueprints-integration/src/api.ts +++ b/packages/blueprints-integration/src/api.ts @@ -5,7 +5,6 @@ import { ConfigManifestEntry } from './config' import { IActionExecutionContext, ISyncIngestUpdateToPartInstanceContext, - IAsRunEventContext, IStudioContext, IPartEventContext, IRundownContext, @@ -14,6 +13,7 @@ import { ISegmentUserContext, IShowStyleUserContext, ICommonContext, + IAsRunEventUserContext, } from './context' import { IngestAdlib, ExtendedIngestRundown, IngestSegment } from './ingest' import { IBlueprintExternalMessageQueueObj } from './message' @@ -180,7 +180,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { ) => PartEndState /** Called after an as-run event is created */ - onAsRunEvent?: (context: IAsRunEventContext) => Promise + onAsRunEvent?: (context: IAsRunEventUserContext) => Promise } export type PartEndState = unknown From f4c58839d95de74d46af46236ddcd9b07eff699b Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 12:48:50 +0100 Subject: [PATCH 34/58] chore: [publish] From ef978590894d249796e9e853f24d3684c6ec43f3 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 13:19:38 +0100 Subject: [PATCH 35/58] feat: ShowStyleBlueprintManifest.onTimelineGenerate changed to have user space context. Unused context argument removed from ShowStyleBlueprintManifest.getEndStateForPart --- packages/blueprints-integration/src/api.ts | 4 ++-- packages/blueprints-integration/src/context.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/blueprints-integration/src/api.ts b/packages/blueprints-integration/src/api.ts index 72326dd481..89e0575136 100644 --- a/packages/blueprints-integration/src/api.ts +++ b/packages/blueprints-integration/src/api.ts @@ -14,6 +14,7 @@ import { IShowStyleUserContext, ICommonContext, IAsRunEventUserContext, + ITimelineEventUserContext, } from './context' import { IngestAdlib, ExtendedIngestRundown, IngestSegment } from './ingest' import { IBlueprintExternalMessageQueueObj } from './message' @@ -163,7 +164,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Called after the timeline has been generated, used to manipulate the timeline */ onTimelineGenerate?: ( - context: ITimelineEventContext, + context: ITimelineEventUserContext, timeline: OnGenerateTimelineObj[], previousPersistentState: TimelinePersistentState | undefined, previousPartEndState: PartEndState | undefined, @@ -172,7 +173,6 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Called just before taking the next part. This generates some persisted data used by onTimelineGenerate to modify the timeline based on the previous part (eg, persist audio levels) */ getEndStateForPart?: ( - context: IRundownContext, previousPersistentState: TimelinePersistentState | undefined, previousPartEndState: PartEndState | undefined, resolvedPieces: IBlueprintResolvedPieceInstance[], diff --git a/packages/blueprints-integration/src/context.ts b/packages/blueprints-integration/src/context.ts index 569625170d..727b20f6cc 100644 --- a/packages/blueprints-integration/src/context.ts +++ b/packages/blueprints-integration/src/context.ts @@ -193,6 +193,8 @@ export interface ITimelineEventContext extends IEventContext, IRundownContext { getTimelineObjectAbSessionId(obj: OnGenerateTimelineObj, sessionName: string): string | undefined } +export interface ITimelineEventUserContext extends ITimelineEventContext, IUserNotesContext {} + export interface IPartEventContext extends IEventContext, IRundownContext { readonly part: Readonly } From 6666546aa70b8c44f4380ddfefe5196a5ef5568c Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 13:19:52 +0100 Subject: [PATCH 36/58] chore: publish From 970c8ea155fb6229c02c410afe0da3c8f78fdfdd Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 13:31:06 +0100 Subject: [PATCH 37/58] fix: remove unused import (linting error) --- packages/blueprints-integration/src/api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/blueprints-integration/src/api.ts b/packages/blueprints-integration/src/api.ts index 89e0575136..b5902a4682 100644 --- a/packages/blueprints-integration/src/api.ts +++ b/packages/blueprints-integration/src/api.ts @@ -8,7 +8,6 @@ import { IStudioContext, IPartEventContext, IRundownContext, - ITimelineEventContext, IStudioUserContext, ISegmentUserContext, IShowStyleUserContext, From 42b571f13b3322b470cd6053170e21b74956b5dc Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 13:32:00 +0100 Subject: [PATCH 38/58] chore: [publish] From 0e30711831ce11366be5ea1fecb595c3279805ac Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 15:47:59 +0100 Subject: [PATCH 39/58] fix: remove unused context argument from StudioBlueprintManifest.getRundownPlaylistInfo, StudioBlueprintManifest.preprocessConfig and ShowStyleBlueprintManifest.getShowStyleVariantId --- packages/blueprints-integration/src/api.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/blueprints-integration/src/api.ts b/packages/blueprints-integration/src/api.ts index b5902a4682..7205acf4bb 100644 --- a/packages/blueprints-integration/src/api.ts +++ b/packages/blueprints-integration/src/api.ts @@ -87,13 +87,10 @@ export interface StudioBlueprintManifest extends BlueprintManifestBase { ) => string | null /** Returns information about the playlist this rundown is a part of, return null to not make it a part of a playlist */ - getRundownPlaylistInfo?: ( - context: IStudioUserContext, - rundowns: IBlueprintRundownDB[] - ) => BlueprintResultRundownPlaylist | null + getRundownPlaylistInfo?: (rundowns: IBlueprintRundownDB[]) => BlueprintResultRundownPlaylist | null /** Preprocess config before storing it by core to later be returned by context's getStudioConfig. If not provided, getStudioConfig will return unprocessed blueprint config */ - preprocessConfig?: (context: ICommonContext, config: IBlueprintConfig) => unknown + preprocessConfig?: (config: IBlueprintConfig) => unknown } export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { @@ -112,7 +109,6 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Returns the id of the show style variant to use for a rundown, return null to ignore that rundown */ getShowStyleVariantId: ( - context: IStudioUserContext, showStyleVariants: IBlueprintShowStyleVariant[], ingestRundown: ExtendedIngestRundown ) => string | null From 97bc8028f0775ea4cfac0c802d6ba90a6cd10417 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 25 Jan 2021 15:48:18 +0100 Subject: [PATCH 40/58] chore: [publish] From 3aecc5118fa5c5a5c008655862349c87c563793b Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 26 Jan 2021 15:23:57 +0100 Subject: [PATCH 41/58] feat: remove impossible interfaces and add type predicate functions for ICommonContext and IUserNotesContext Co-authored-by: Julian Waller --- .../src/__tests__/context.spec.ts | 155 ++++++++++++++++++ .../blueprints-integration/src/context.ts | 31 +++- 2 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 packages/blueprints-integration/src/__tests__/context.spec.ts diff --git a/packages/blueprints-integration/src/__tests__/context.spec.ts b/packages/blueprints-integration/src/__tests__/context.spec.ts new file mode 100644 index 0000000000..3cc9a751d4 --- /dev/null +++ b/packages/blueprints-integration/src/__tests__/context.spec.ts @@ -0,0 +1,155 @@ +import { isCommonContext, isUserNotesContext } from '../context' + +describe('Context', () => { + const validCommonContext = { + getHashId: () => 'fake', + unhashId: () => 'more fake', + logDebug: () => undefined, + logInfo: () => undefined, + logWarning: () => undefined, + logError: () => undefined, + } + describe('ICommonContext predicate function', () => { + { + it('should return false for undefined', () => { + expect(isCommonContext(undefined)).toBe(false) + }) + + it('should return false for null', () => { + expect(isCommonContext(null)).toBe(false) + }) + + it('should return false for literal value', () => { + expect(isCommonContext('hehe')).toBe(false) + }) + + it('should return false for an object where getHashId is missing', () => { + const invalid = Object.assign({}, validCommonContext, { getHashId: undefined }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where getHashId is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { getHashId: { hehe: 'lol' } }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where unhashId is missing', () => { + const invalid = Object.assign({}, validCommonContext, { unhashId: undefined }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where unhashId is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { unhashId: { hehe: 'lol' } }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logDebug is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logDebug: undefined }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logDebug is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logDebug: { hehe: 'lol' } }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logInfo is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logInfo: undefined }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logInfo is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logInfo: { hehe: 'lol' } }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logWarning is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logWarning: undefined }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logWarning is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logWarning: { hehe: 'lol' } }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logError is missing', () => { + const invalid = Object.assign({}, validCommonContext, { logError: undefined }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return false for an object where logError is not a function', () => { + const invalid = Object.assign({}, validCommonContext, { logError: { hehe: 'lol' } }) + + expect(isCommonContext(invalid)).toBe(false) + }) + + it('should return true for a valid context', () => { + expect(isCommonContext(validCommonContext)).toBe(true) + }) + } + }) + + describe('IUserNotesContext predicate function', () => { + const userNotesContextMethods = { + notifyUserError: () => undefined, + notifyUserWarning: () => undefined, + } + const validUserNotesContext = Object.assign({}, validCommonContext, userNotesContextMethods) + + it('should return false for undefined', () => { + expect(isUserNotesContext(undefined)).toBe(false) + }) + + it('should return false for null', () => { + expect(isUserNotesContext(null)).toBe(false) + }) + + it('should return false for literal value', () => { + expect(isUserNotesContext('hehe')).toBe(false) + }) + + it('should return false when object is not a Common Context implementation', () => { + expect(isUserNotesContext(Object.assign({}, userNotesContextMethods))).toBe(false) + }) + + it('should return false for an object where notifyUserError is missing', () => { + const invalid = Object.assign({}, validUserNotesContext, { notifyUserError: undefined }) + + expect(isUserNotesContext(invalid)).toBe(false) + }) + + it('should return false for an object where notifyUserError is not a function', () => { + const invalid = Object.assign({}, validUserNotesContext, { notifyUserError: { hehe: 'lol' } }) + + expect(isUserNotesContext(invalid)).toBe(false) + }) + + it('should return false for an object where notifyUserWarning is missing', () => { + const invalid = Object.assign({}, validUserNotesContext, { notifyUserWarning: undefined }) + + expect(isUserNotesContext(invalid)).toBe(false) + }) + + it('should return false for an object where notifyUserWarning is not a function', () => { + const invalid = Object.assign({}, validUserNotesContext, { notifyUserWarning: { hehe: 'lol' } }) + + expect(isUserNotesContext(invalid)).toBe(false) + }) + + it('should return true for a valid context', () => { + expect(isUserNotesContext(validUserNotesContext)).toBe(true) + }) + }) +}) diff --git a/packages/blueprints-integration/src/context.ts b/packages/blueprints-integration/src/context.ts index 727b20f6cc..79dc1138b5 100644 --- a/packages/blueprints-integration/src/context.ts +++ b/packages/blueprints-integration/src/context.ts @@ -37,6 +37,23 @@ export interface ICommonContext { logError: (message: string) => void } +export function isCommonContext(obj: unknown): obj is ICommonContext { + if (!obj || typeof obj !== 'object') { + return false + } + + const { getHashId, unhashId, logDebug, logInfo, logWarning, logError } = obj as any + + return ( + typeof getHashId === 'function' && + typeof unhashId === 'function' && + typeof logDebug === 'function' && + typeof logInfo === 'function' && + typeof logWarning === 'function' && + typeof logError === 'function' + ) +} + export interface IUserNotesContext extends ICommonContext { /** Display a notification to the user of an error */ notifyUserError(message: string, params?: { [key: string]: any }): void @@ -44,6 +61,16 @@ export interface IUserNotesContext extends ICommonContext { notifyUserWarning(message: string, params?: { [key: string]: any }): void } +export function isUserNotesContext(obj: unknown): obj is IUserNotesContext { + if (!isCommonContext(obj)) { + return false + } + + const { notifyUserError, notifyUserWarning } = obj as any + + return typeof notifyUserError === 'function' && typeof notifyUserWarning === 'function' +} + /** Studio */ export interface IStudioContext extends ICommonContext { @@ -193,8 +220,6 @@ export interface ITimelineEventContext extends IEventContext, IRundownContext { getTimelineObjectAbSessionId(obj: OnGenerateTimelineObj, sessionName: string): string | undefined } -export interface ITimelineEventUserContext extends ITimelineEventContext, IUserNotesContext {} - export interface IPartEventContext extends IEventContext, IRundownContext { readonly part: Readonly } @@ -253,5 +278,3 @@ export interface IAsRunEventContext extends IEventContext, IRundownContext { /** Get the ingest data related to a partInstance */ getIngestDataForPartInstance(partInstance: Readonly): Readonly | undefined } - -export interface IAsRunEventUserContext extends IAsRunEventContext, IUserNotesContext {} From 2ec34968cbac5a2069c10fd14ef5236de1d22f2b Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 26 Jan 2021 15:25:52 +0100 Subject: [PATCH 42/58] chore: [publish] From cfc0a1b40d23e59cfa3d2965f1480893c59f2204 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 26 Jan 2021 15:37:23 +0100 Subject: [PATCH 43/58] fix: change back to non user contexts for ShowStyleBlueprintManifest.onTimelineGenerate and onAsRunEvent --- packages/blueprints-integration/src/api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/blueprints-integration/src/api.ts b/packages/blueprints-integration/src/api.ts index 7205acf4bb..d530daa72b 100644 --- a/packages/blueprints-integration/src/api.ts +++ b/packages/blueprints-integration/src/api.ts @@ -12,8 +12,8 @@ import { ISegmentUserContext, IShowStyleUserContext, ICommonContext, - IAsRunEventUserContext, - ITimelineEventUserContext, + IAsRunEventContext, + ITimelineEventContext, } from './context' import { IngestAdlib, ExtendedIngestRundown, IngestSegment } from './ingest' import { IBlueprintExternalMessageQueueObj } from './message' @@ -159,7 +159,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Called after the timeline has been generated, used to manipulate the timeline */ onTimelineGenerate?: ( - context: ITimelineEventUserContext, + context: ITimelineEventContext, timeline: OnGenerateTimelineObj[], previousPersistentState: TimelinePersistentState | undefined, previousPartEndState: PartEndState | undefined, @@ -175,7 +175,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { ) => PartEndState /** Called after an as-run event is created */ - onAsRunEvent?: (context: IAsRunEventUserContext) => Promise + onAsRunEvent?: (context: IAsRunEventContext) => Promise } export type PartEndState = unknown From 52fdfb11cdda4ce5f650541e3abfaac6ef8994b6 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 26 Jan 2021 15:37:31 +0100 Subject: [PATCH 44/58] chore: [publish] From 903c6ab49f495fea7f25287826f854b96b3419a1 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 27 Jan 2021 11:20:46 +0100 Subject: [PATCH 45/58] fix: bring back context argument for ShowStyleBlueprintManifest.getEndStateForPart --- packages/blueprints-integration/src/api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/blueprints-integration/src/api.ts b/packages/blueprints-integration/src/api.ts index d530daa72b..aab49226e5 100644 --- a/packages/blueprints-integration/src/api.ts +++ b/packages/blueprints-integration/src/api.ts @@ -168,6 +168,7 @@ export interface ShowStyleBlueprintManifest extends BlueprintManifestBase { /** Called just before taking the next part. This generates some persisted data used by onTimelineGenerate to modify the timeline based on the previous part (eg, persist audio levels) */ getEndStateForPart?: ( + context: IRundownContext, previousPersistentState: TimelinePersistentState | undefined, previousPartEndState: PartEndState | undefined, resolvedPieces: IBlueprintResolvedPieceInstance[], From 6935412394fef735e54275abd1bfc4dbb651afad Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 27 Jan 2021 11:21:59 +0100 Subject: [PATCH 46/58] chore: [publish] From 6a8ac286957ac60d2b9b7c34cab795033a575f1e Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 1 Feb 2021 13:47:01 +0100 Subject: [PATCH 47/58] fix: update blueprints-integration dependency and re-add context where necessary --- meteor/__mocks__/helpers/database.ts | 1 - meteor/package-lock.json | 2 +- meteor/server/api/blueprints/config.ts | 10 +++---- meteor/server/api/rundown.ts | 37 +++++++++++++------------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/meteor/__mocks__/helpers/database.ts b/meteor/__mocks__/helpers/database.ts index a8331c475a..713af95b8a 100644 --- a/meteor/__mocks__/helpers/database.ts +++ b/meteor/__mocks__/helpers/database.ts @@ -313,7 +313,6 @@ export function setupMockShowStyleBlueprint(showStyleVariantId: ShowStyleVariant showStyleConfigManifest: [], showStyleMigrations: [], getShowStyleVariantId: ( - context: unknown, showStyleVariants: Array, ingestRundown: IngestRundown ): string | null => { diff --git a/meteor/package-lock.json b/meteor/package-lock.json index 76c862d2f5..21d3f68447 100644 --- a/meteor/package-lock.json +++ b/meteor/package-lock.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.17.0-in-testing-R29.1", + "version": "1.18.0-in-development-R30", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/meteor/server/api/blueprints/config.ts b/meteor/server/api/blueprints/config.ts index 2f1eca1345..44171a3bd8 100644 --- a/meteor/server/api/blueprints/config.ts +++ b/meteor/server/api/blueprints/config.ts @@ -96,11 +96,11 @@ export function preprocessStudioConfig(studio: Studio, blueprint?: StudioBluepri res['SofieHostURL'] = studio.settings.sofieUrl if (blueprint && blueprint.preprocessConfig) { - const context = new CommonContext({ - name: `preprocessStudioConfig`, - identifier: `studioId=${studio._id}`, - }) - res = blueprint.preprocessConfig(context, res) + // const context = new CommonContext({ + // name: `preprocessStudioConfig`, + // identifier: `studioId=${studio._id}`, + // }) + res = blueprint.preprocessConfig(res) } return res } diff --git a/meteor/server/api/rundown.ts b/meteor/server/api/rundown.ts index 1ef593b30c..e5a63f7321 100644 --- a/meteor/server/api/rundown.ts +++ b/meteor/server/api/rundown.ts @@ -124,7 +124,6 @@ export function selectShowStyleVariant( const variantId: ShowStyleVariantId | null = protectString( showStyleBlueprint.blueprint.getShowStyleVariantId( - context, unprotectObjectArray(showStyleVariants) as any, ingestRundown ) @@ -203,14 +202,14 @@ export function produceRundownPlaylistRanks( const playlistInfo: BlueprintResultRundownPlaylist | null = studioBlueprint.blueprint.getRundownPlaylistInfo ? studioBlueprint.blueprint.getRundownPlaylistInfo( - new StudioUserContext( - { - name: 'produceRundownPlaylistRanks', - identifier: `studioId=${studio._id},playlistId=${unprotectString(playlistId)}`, - tempSendUserNotesIntoBlackHole: true, - }, - studio - ), + // new StudioUserContext( + // { + // name: 'produceRundownPlaylistRanks', + // identifier: `studioId=${studio._id},playlistId=${unprotectString(playlistId)}`, + // tempSendUserNotesIntoBlackHole: true, + // }, + // studio + // ), unprotectObjectArray(rundowns) ) : null @@ -269,16 +268,16 @@ export function produceRundownPlaylistInfoFromRundown( const playlistInfo: BlueprintResultRundownPlaylist | null = studioBlueprint.blueprint.getRundownPlaylistInfo ? studioBlueprint.blueprint.getRundownPlaylistInfo( - new StudioUserContext( - { - name: 'produceRundownPlaylistInfoFromRundown', - identifier: `studioId=${studio._id},playlistId=${unprotectString( - playlistId - )},rundownId=${currentRundown._id}`, - tempSendUserNotesIntoBlackHole: true, - }, - studio - ), + // new StudioUserContext( + // { + // name: 'produceRundownPlaylistInfoFromRundown', + // identifier: `studioId=${studio._id},playlistId=${unprotectString( + // playlistId + // )},rundownId=${currentRundown._id}`, + // tempSendUserNotesIntoBlackHole: true, + // }, + // studio + // ), unprotectObjectArray(rundowns) ) : null From c957ec87cba4ac18e59aa2eec13b3f92d9af673d Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 1 Feb 2021 13:50:40 +0100 Subject: [PATCH 48/58] feat: working import and translations from blueprint content --- meteor/client/ui/i18n.ts | 22 +++++-- meteor/lib/collections/TranslationsBundles.ts | 29 ++++++++- meteor/server/api/translationsBundles.ts | 61 ++++++++++++++----- 3 files changed, 90 insertions(+), 22 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 5211047f56..ce5e66d64e 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -5,7 +5,8 @@ import LanguageDetector from 'i18next-browser-languagedetector' import { initReactI18next } from 'react-i18next' import { WithManagedTracker } from '../lib/reactiveData/reactiveDataHelper' import { PubSub } from '../../lib/api/pubsub' -import { TranslationsBundles } from '../../lib/collections/TranslationsBundles' +import { Translation, TranslationsBundles } from '../../lib/collections/TranslationsBundles' +import { I18NextData } from '@sofie-automation/blueprints-integration' const i18nOptions = { fallbackLng: { @@ -37,6 +38,15 @@ const i18nOptions = { }, } +function toI18NextData(translations: Translation[]): I18NextData { + const data = {} + for (const { original, translation } of translations) { + data[original] = translation + } + + return data +} + class I18nContainer extends WithManagedTracker { i18nInstance: typeof i18n @@ -64,11 +74,13 @@ class I18nContainer extends WithManagedTracker { const bundles = TranslationsBundles.find().fetch() console.debug(`Got ${bundles.length} bundles from database`) for (const bundle of bundles) { - if (Object.keys(bundle.data).length > 0) { + if (bundle.data.length > 0) { + const i18NextData = toI18NextData(bundle.data) + this.i18nInstance.addResourceBundle( bundle.language, bundle.namespace || i18nOptions.defaultNS, - bundle.data, + i18NextData, true, true ) @@ -104,10 +116,10 @@ class I18nContainer extends WithManagedTracker { Object.assign(options, { ...options.replace }) } - const interpolated = String(key) + let interpolated = String(key) for (const placeholder of key.match(/[^{\}]+(?=})/g) || []) { const value = options[placeholder] || placeholder - interpolated.replace(`{{${placeholder}}}`, value) + interpolated = interpolated.replace(`{{${placeholder}}}`, value) } return interpolated diff --git a/meteor/lib/collections/TranslationsBundles.ts b/meteor/lib/collections/TranslationsBundles.ts index dd96037266..7a60b43e4f 100644 --- a/meteor/lib/collections/TranslationsBundles.ts +++ b/meteor/lib/collections/TranslationsBundles.ts @@ -1,14 +1,39 @@ import { TransformedCollection } from '../typings/meteor' import { registerCollection, ProtectedString } from '../lib' -import { TranslationsBundle as BlueprintTranslationsBundle } from '@sofie-automation/blueprints-integration' +import { TranslationsBundleType } from '@sofie-automation/blueprints-integration' import { createMongoCollection } from './lib' /** A string identifying a translations bundle */ export type TranslationsBundleId = ProtectedString<'TranslationsBundleId'> -export interface TranslationsBundle extends BlueprintTranslationsBundle { +export type Translation = { original: string; translation: string } + +/** + * Interface for the DB collection type for translation bundles. + * + * Note that this interface is slightly divergent from the TranslationsBundle + * type used by the blueprints, specifically in the data property. + * + * The reason for this is that (Mini)Mongo does not allow property names with dots, + * so using the literal original strings (which frequently have punctuation) as + * property names won't work. Therefore it is stored to the database as an array + * of object with explicitly names original and translated properties. + */ +export interface TranslationsBundle { _id: TranslationsBundleId + + type: TranslationsBundleType + + /** language code (example: 'nb'), annotates what language the translations are for */ + language: string + /** optional namespace for the bundle */ + namespace?: string + /** encoding used for the data, typically utf-8 */ + encoding?: string + + /** the actual translations */ + data: Translation[] } export const TranslationsBundles: TransformedCollection = createMongoCollection< diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index b504e658a4..0002ddf481 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -1,10 +1,26 @@ -import { TranslationsBundles, TranslationsBundleId } from '../../lib/collections/TranslationsBundles' -import { TranslationsBundle, TranslationsBundleType } from '@sofie-automation/blueprints-integration' +import { + TranslationsBundles as TranslationsBundleCollection, + TranslationsBundleId, + Translation, + TranslationsBundle as DBTranslationsBundle, +} from '../../lib/collections/TranslationsBundles' +import { + I18NextData, + TranslationsBundle as BlueprintTranslationsbundle, + TranslationsBundleType, +} from '@sofie-automation/blueprints-integration' import { getRandomId, unprotectString } from '../../lib/lib' import { logger } from '../logging' import { BlueprintId } from '../../lib/collections/Blueprints' +import { Mongocursor } from '../../lib/typings/meteor' -export function upsertBundles(bundles: TranslationsBundle[], parentBlueprintId: BlueprintId) { +/** + * Insert or update translation bundles in the database. + * + * @param bundles the bundles to insert or update + * @param parentBlueprintId id of the blueprint the translation bundles belongs to + */ +export function upsertBundles(bundles: BlueprintTranslationsbundle[], parentBlueprintId: BlueprintId) { for (const bundle of bundles) { const { type, language, data } = bundle @@ -15,32 +31,47 @@ export function upsertBundles(bundles: TranslationsBundle[], parentBlueprintId: const namespace = unprotectString(parentBlueprintId) const _id = getExistingId(namespace, language) || getRandomId<'TranslationsBundleId'>() - TranslationsBundles.upsert( + TranslationsBundleCollection.upsert( _id, - { _id, type, namespace, language, data }, + { _id, type, namespace, language, data: fromI18NextData(data) }, { multi: false }, - ( - err: Error, - { numberAffected, insertedId }: { numberAffected: number; insertedId?: TranslationsBundleId } - ) => { + (err: Error, numberAffected: number) => { if (!err && numberAffected) { - logger.info(`Stored translation bundle ([${insertedId || _id}]:${namespace}:${language})`) + logger.info(`Stored${_id ? '' : ' new '}translation bundle :${namespace}:${language})`) } else { logger.error(`Unable to store translation bundle ([${_id}]:${namespace}:${language})`, { error: err, }) } - const dbCursor = TranslationsBundles.find({}) - const availableBundles = dbCursor.count() - const bundles = dbCursor.fetch().map(({ _id, namespace, language }) => ({ _id, namespace, language })) - logger.debug(`${availableBundles} bundles in database:`, { bundles }) + const dbCursor = TranslationsBundleCollection.find({}) + logger.debug(`${dbCursor.count()} bundles in database:`, { bundles: fetchAvailableBundles(dbCursor) }) } ) } } function getExistingId(namespace: string | undefined, language: string): TranslationsBundleId | null { - const bundle = TranslationsBundles.findOne({ namespace, language }) + const bundle = TranslationsBundleCollection.findOne({ namespace, language }) return bundle ? bundle._id : null } + +function fetchAvailableBundles(dbCursor: Mongocursor<{ _id: TranslationsBundleId } & DBTranslationsBundle>) { + const stuff = dbCursor.fetch() + return stuff.map(({ namespace, language }) => ({ namespace, language })) +} + +/** + * Convert data from the i18next form which the blueprint type specifies into a format that Mongo accepts. + * + * @param data translations on i18next form + * @returns translations suitable to put into Mongo + */ +function fromI18NextData(data: I18NextData): Translation[] { + const translations: Translation[] = [] + for (const original in data) { + translations.push({ original, translation: data[original] }) + } + + return translations +} From aeb100dd623777feb59451e74aba7a7fa50cfe1e Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 1 Feb 2021 14:23:42 +0100 Subject: [PATCH 49/58] chore: [publish] From 5a636a7394588a4339c91da073cd34ad290a4974 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Mon, 1 Feb 2021 15:11:02 +0100 Subject: [PATCH 50/58] chore: removed unused imports --- meteor/server/api/blueprints/context/context.ts | 2 -- .../api/blueprints/context/syncIngestUpdateToPartInstance.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/meteor/server/api/blueprints/context/context.ts b/meteor/server/api/blueprints/context/context.ts index 2510739ee9..6439147c2e 100644 --- a/meteor/server/api/blueprints/context/context.ts +++ b/meteor/server/api/blueprints/context/context.ts @@ -1,4 +1,3 @@ -import * as objectPath from 'object-path' import { Meteor } from 'meteor/meteor' import { getHash, @@ -32,7 +31,6 @@ import { IBlueprintAsRunLogEvent, IBlueprintExternalMessageQueueObj, ExtendedIngestRundown, - OnGenerateTimelineObj, IShowStyleContext, IRundownContext, IEventContext, diff --git a/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts b/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts index d8474dbc54..662654f907 100644 --- a/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts +++ b/meteor/server/api/blueprints/context/syncIngestUpdateToPartInstance.ts @@ -24,7 +24,6 @@ import { protectString, protectStringArray, unprotectStringArray, - normalizeArray, normalizeArrayToMap, } from '../../../../lib/lib' import { Rundown } from '../../../../lib/collections/Rundowns' From 835b14c33c5a719a2698d9d2845b492a4d74dd8f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 2 Feb 2021 13:55:15 +0100 Subject: [PATCH 51/58] feat: implment a method to fetch individual translation bundles Also, cache those bundles in localStorage so that we don't re-fetch them all the time. --- meteor/client/ui/i18n.ts | 86 +++++++++++++++---- meteor/lib/api/userActions.ts | 4 + meteor/lib/collections/TranslationsBundles.ts | 3 + meteor/server/api/translationsBundles.ts | 14 ++- meteor/server/api/userActions.ts | 11 +++ .../publications/translationsBundles.ts | 6 +- 6 files changed, 102 insertions(+), 22 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index ce5e66d64e..ec204702ef 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -5,8 +5,15 @@ import LanguageDetector from 'i18next-browser-languagedetector' import { initReactI18next } from 'react-i18next' import { WithManagedTracker } from '../lib/reactiveData/reactiveDataHelper' import { PubSub } from '../../lib/api/pubsub' -import { Translation, TranslationsBundles } from '../../lib/collections/TranslationsBundles' +import { + Translation, + TranslationsBundle, + TranslationsBundleId, + TranslationsBundles, +} from '../../lib/collections/TranslationsBundles' import { I18NextData } from '@sofie-automation/blueprints-integration' +import { MeteorCall } from '../../lib/api/methods' +import { ClientAPI } from '../../lib/api/client' const i18nOptions = { fallbackLng: { @@ -47,6 +54,24 @@ function toI18NextData(translations: Translation[]): I18NextData { return data } +function getAndCacheTranslationBundle(bundleId: TranslationsBundleId) { + return new Promise((resolve, reject) => + MeteorCall.userAction.getTranslationBundle(bundleId).then( + (response) => { + if (ClientAPI.isClientResponseSuccess(response) && response.result) { + localStorage.setItem(`i18n.translationBundles.${bundleId}`, JSON.stringify(response.result)) + resolve(response.result) + } else { + reject(response) + } + }, + (reason) => { + reject(reason) + } + ) + ) +} + class I18nContainer extends WithManagedTracker { i18nInstance: typeof i18n @@ -71,24 +96,47 @@ class I18nContainer extends WithManagedTracker { this.subscribe(PubSub.translationsBundles, {}) this.autorun(() => { console.debug('ManagedTracker autorun...') - const bundles = TranslationsBundles.find().fetch() - console.debug(`Got ${bundles.length} bundles from database`) - for (const bundle of bundles) { - if (bundle.data.length > 0) { - const i18NextData = toI18NextData(bundle.data) - - this.i18nInstance.addResourceBundle( - bundle.language, - bundle.namespace || i18nOptions.defaultNS, - i18NextData, - true, - true - ) - console.debug('i18instance updated', { bundle: { lang: bundle.language, ns: bundle.namespace } }) - } else { - console.debug(`Skipped bundle, no translations`, { bundle }) - } - } + const bundlesInfo = TranslationsBundles.find().fetch() as Omit[] + console.debug(`Got ${bundlesInfo.length} bundles from database`) + Promise.allSettled( + bundlesInfo.map((bundle) => + new Promise((resolve, reject) => { + const bundleString = localStorage.getItem(`i18n.translationBundles.${bundle._id}`) + if (bundleString) { + // check hash + try { + const bundleObj = JSON.parse(bundleString) as TranslationsBundle + if (bundleObj.hash === bundle.hash) { + resolve(bundleObj) // the cached bundle is up-to-date + return + } + } finally { + // the cache seems to be corrupt, we re-fetch from backend + resolve(getAndCacheTranslationBundle(bundle._id)) + } + } else { + resolve(getAndCacheTranslationBundle(bundle._id)) + } + }) + .then((bundle) => { + const i18NextData = toI18NextData(bundle.data) + + this.i18nInstance.addResourceBundle( + bundle.language, + bundle.namespace || i18nOptions.defaultNS, + i18NextData, + true, + true + ) + console.debug('i18instance updated', { + bundle: { lang: bundle.language, ns: bundle.namespace }, + }) + }) + .catch((reason) => { + console.error(`Failed to fetch translations bundle "${bundle._id}": `, reason) + }) + ) + ).then(() => console.debug(`Finished updating ${bundlesInfo.length} translation bundles`)) }) } // return key until real translator comes online diff --git a/meteor/lib/api/userActions.ts b/meteor/lib/api/userActions.ts index ad00159859..d9f176e63c 100644 --- a/meteor/lib/api/userActions.ts +++ b/meteor/lib/api/userActions.ts @@ -17,6 +17,7 @@ import { IngestAdlib, ActionUserData } from '@sofie-automation/blueprints-integr import { BucketAdLib } from '../collections/BucketAdlibs' import { AdLibActionId, AdLibActionCommon } from '../collections/AdLibActions' import { BucketAdLibAction } from '../collections/BucketAdlibActions' +import { TranslationsBundle, TranslationsBundleId } from '../collections/TranslationsBundles' export interface NewUserActionAPI extends MethodContext { take(userEvent: string, rundownPlaylistId: RundownPlaylistId): Promise> @@ -160,6 +161,7 @@ export interface NewUserActionAPI extends MethodContext { restartCore(userEvent: string, token: string): Promise> guiFocused(userEvent: string, viewInfo?: any[]): Promise> guiBlurred(userEvent: string, viewInfo?: any[]): Promise> + getTranslationBundle(bundleId: TranslationsBundleId): Promise> bucketsRemoveBucket(userEvent: string, id: BucketId): Promise> bucketsModifyBucket( userEvent: string, @@ -271,6 +273,8 @@ export enum UserActionAPIMethods { 'guiFocused' = 'userAction.focused', 'guiBlurred' = 'userAction.blurred', + 'getTranslationBundle' = 'userAction.getTranslationBundle', + 'switchRouteSet' = 'userAction.switchRouteSet', 'moveRundown' = 'userAction.moveRundown', 'restoreRundownOrder' = 'userAction.restoreRundownOrder', diff --git a/meteor/lib/collections/TranslationsBundles.ts b/meteor/lib/collections/TranslationsBundles.ts index 7a60b43e4f..17a5ab56da 100644 --- a/meteor/lib/collections/TranslationsBundles.ts +++ b/meteor/lib/collections/TranslationsBundles.ts @@ -32,6 +32,9 @@ export interface TranslationsBundle { /** encoding used for the data, typically utf-8 */ encoding?: string + /** A unique hash of the `data` object, to signal that the contents have updated */ + hash: string + /** the actual translations */ data: Translation[] } diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index 0002ddf481..3a584c3fc4 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -1,3 +1,4 @@ +import { Meteor } from 'meteor/meteor' import { TranslationsBundles as TranslationsBundleCollection, TranslationsBundleId, @@ -9,7 +10,7 @@ import { TranslationsBundle as BlueprintTranslationsbundle, TranslationsBundleType, } from '@sofie-automation/blueprints-integration' -import { getRandomId, unprotectString } from '../../lib/lib' +import { getHash, getRandomId, unprotectString } from '../../lib/lib' import { logger } from '../logging' import { BlueprintId } from '../../lib/collections/Blueprints' import { Mongocursor } from '../../lib/typings/meteor' @@ -33,7 +34,7 @@ export function upsertBundles(bundles: BlueprintTranslationsbundle[], parentBlue TranslationsBundleCollection.upsert( _id, - { _id, type, namespace, language, data: fromI18NextData(data) }, + { _id, type, namespace, language, data: fromI18NextData(data), hash: getHash(JSON.stringify(data)) }, { multi: false }, (err: Error, numberAffected: number) => { if (!err && numberAffected) { @@ -61,6 +62,15 @@ function fetchAvailableBundles(dbCursor: Mongocursor<{ _id: TranslationsBundleId return stuff.map(({ namespace, language }) => ({ namespace, language })) } +export function getBundle(bundleId: TranslationsBundleId) { + const bundle = TranslationsBundleCollection.findOne(bundleId) + if (!bundle) { + throw new Meteor.Error(404, `Bundle "${bundleId}" not found`) + } + + return bundle +} + /** * Convert data from the i18next form which the blueprint type specifies into a format that Mongo accepts. * diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index b55592f50d..3c58f547f5 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -50,6 +50,8 @@ import { profiler } from './profiler' import { AdLibActionId, AdLibActionCommon } from '../../lib/collections/AdLibActions' import { BucketAdLibAction } from '../../lib/collections/BucketAdlibActions' import { checkAccessAndGetPlaylist, checkAccessAndGetRundown } from './lib' +import { TranslationsBundle, TranslationsBundleId } from '../../lib/collections/TranslationsBundles' +import { getBundle as getTranslationBundleInner } from './translationsBundles' let MINIMUM_TAKE_SPAN = 1000 export function setMinimumTakeSpan(span: number) { @@ -593,6 +595,12 @@ export function mediaAbortAllWorkflows(context: MethodContext) { const access = OrganizationContentWriteAccess.anyContent(context) return ClientAPI.responseSuccess(MediaManagerAPI.abortAllWorkflows(context, access.organizationId)) } +export function getTranslationBundle(context: MethodContext, bundleId: TranslationsBundleId) { + check(bundleId, String) + + const access = OrganizationContentWriteAccess.anyContent(context) + return ClientAPI.responseSuccess(getTranslationBundleInner(bundleId)) +} export function bucketsRemoveBucket(context: MethodContext, id: BucketId) { check(id, String) @@ -1058,6 +1066,9 @@ class ServerUserActionAPI extends MethodContextAPI implements NewUserActionAPI { guiBlurred(_userEvent: string, _viewInfo: any[]) { return traceAction('userAction.noop', noop, this) } + getTranslationBundle(bundleId: TranslationsBundleId): Promise> { + return makePromise(() => getTranslationBundle(this, bundleId)) + } bucketsRemoveBucket(_userEvent: string, id: BucketId) { return traceAction(UserActionAPIMethods.bucketsRemoveBucket, bucketsRemoveBucket, this, id) } diff --git a/meteor/server/publications/translationsBundles.ts b/meteor/server/publications/translationsBundles.ts index bf825d46ad..43fe486ad9 100644 --- a/meteor/server/publications/translationsBundles.ts +++ b/meteor/server/publications/translationsBundles.ts @@ -9,7 +9,11 @@ meteorPublish(PubSub.translationsBundles, (selector, token) => { if (!selector) throw new Meteor.Error(400, 'selector argument missing') if (TranslationsBundlesSecurity.allowReadAccess(selector, token, this)) { - return TranslationsBundles.find(selector) + return TranslationsBundles.find(selector, { + fields: { + data: 0, + }, + }) } return null From 79ccef2a883d1cefb0ab85fc5151b005a15a6a81 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Tue, 2 Feb 2021 13:57:00 +0100 Subject: [PATCH 52/58] chore: remove unused reject function --- meteor/client/ui/i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index ec204702ef..eef9c0700a 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -100,7 +100,7 @@ class I18nContainer extends WithManagedTracker { console.debug(`Got ${bundlesInfo.length} bundles from database`) Promise.allSettled( bundlesInfo.map((bundle) => - new Promise((resolve, reject) => { + new Promise((resolve) => { const bundleString = localStorage.getItem(`i18n.translationBundles.${bundle._id}`) if (bundleString) { // check hash From 498d4c0135d66b151626bb96fe1b72c44c7b0577 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 2 Feb 2021 16:09:31 +0100 Subject: [PATCH 53/58] chore: sonarlint fix --- meteor/client/ui/i18n.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index eef9c0700a..359eeae061 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -99,23 +99,23 @@ class I18nContainer extends WithManagedTracker { const bundlesInfo = TranslationsBundles.find().fetch() as Omit[] console.debug(`Got ${bundlesInfo.length} bundles from database`) Promise.allSettled( - bundlesInfo.map((bundle) => + bundlesInfo.map((bundleMetadata) => new Promise((resolve) => { - const bundleString = localStorage.getItem(`i18n.translationBundles.${bundle._id}`) + const bundleString = localStorage.getItem(`i18n.translationBundles.${bundleMetadata._id}`) if (bundleString) { // check hash try { const bundleObj = JSON.parse(bundleString) as TranslationsBundle - if (bundleObj.hash === bundle.hash) { + if (bundleObj.hash === bundleMetadata.hash) { resolve(bundleObj) // the cached bundle is up-to-date return } } finally { // the cache seems to be corrupt, we re-fetch from backend - resolve(getAndCacheTranslationBundle(bundle._id)) + resolve(getAndCacheTranslationBundle(bundleMetadata._id)) } } else { - resolve(getAndCacheTranslationBundle(bundle._id)) + resolve(getAndCacheTranslationBundle(bundleMetadata._id)) } }) .then((bundle) => { @@ -133,7 +133,7 @@ class I18nContainer extends WithManagedTracker { }) }) .catch((reason) => { - console.error(`Failed to fetch translations bundle "${bundle._id}": `, reason) + console.error(`Failed to fetch translations bundle "${bundleMetadata._id}": `, reason) }) ) ).then(() => console.debug(`Finished updating ${bundlesInfo.length} translation bundles`)) From 92e7e3dc5291e051803d2ac0d1ad43bac0f105cd Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 2 Feb 2021 16:45:55 +0100 Subject: [PATCH 54/58] feat: explicitly tie origin blueprint to translation bundle in data model --- meteor/lib/collections/TranslationsBundles.ts | 4 ++ meteor/server/api/translationsBundles.ts | 40 ++++++++++++++----- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/meteor/lib/collections/TranslationsBundles.ts b/meteor/lib/collections/TranslationsBundles.ts index 17a5ab56da..ae8f724405 100644 --- a/meteor/lib/collections/TranslationsBundles.ts +++ b/meteor/lib/collections/TranslationsBundles.ts @@ -3,6 +3,7 @@ import { registerCollection, ProtectedString } from '../lib' import { TranslationsBundleType } from '@sofie-automation/blueprints-integration' import { createMongoCollection } from './lib' +import { BlueprintId } from './Blueprints' /** A string identifying a translations bundle */ export type TranslationsBundleId = ProtectedString<'TranslationsBundleId'> @@ -25,6 +26,9 @@ export interface TranslationsBundle { type: TranslationsBundleType + /** the id of the blueprint the translations were bundled with */ + originBlueprintId: BlueprintId + /** language code (example: 'nb'), annotates what language the translations are for */ language: string /** optional namespace for the bundle */ diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index 3a584c3fc4..33dfa3e69f 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -10,7 +10,7 @@ import { TranslationsBundle as BlueprintTranslationsbundle, TranslationsBundleType, } from '@sofie-automation/blueprints-integration' -import { getHash, getRandomId, unprotectString } from '../../lib/lib' +import { getHash, protectString, unprotectString } from '../../lib/lib' import { logger } from '../logging' import { BlueprintId } from '../../lib/collections/Blueprints' import { Mongocursor } from '../../lib/typings/meteor' @@ -19,9 +19,9 @@ import { Mongocursor } from '../../lib/typings/meteor' * Insert or update translation bundles in the database. * * @param bundles the bundles to insert or update - * @param parentBlueprintId id of the blueprint the translation bundles belongs to + * @param originBlueprintId id of the blueprint the translation bundles belongs to */ -export function upsertBundles(bundles: BlueprintTranslationsbundle[], parentBlueprintId: BlueprintId) { +export function upsertBundles(bundles: BlueprintTranslationsbundle[], originBlueprintId: BlueprintId) { for (const bundle of bundles) { const { type, language, data } = bundle @@ -29,18 +29,25 @@ export function upsertBundles(bundles: BlueprintTranslationsbundle[], parentBlue throw new Error(`Unknown bundle type ${type}`) } - const namespace = unprotectString(parentBlueprintId) - const _id = getExistingId(namespace, language) || getRandomId<'TranslationsBundleId'>() + const _id = getExistingId(originBlueprintId, language) || createBundleId(originBlueprintId, language) TranslationsBundleCollection.upsert( _id, - { _id, type, namespace, language, data: fromI18NextData(data), hash: getHash(JSON.stringify(data)) }, + { + _id, + originBlueprintId, + type, + namespace: unprotectString(originBlueprintId), + language, + data: fromI18NextData(data), + hash: getHash(JSON.stringify(data)), + }, { multi: false }, (err: Error, numberAffected: number) => { if (!err && numberAffected) { - logger.info(`Stored${_id ? '' : ' new '}translation bundle :${namespace}:${language})`) + logger.info(`Stored ${_id ? '' : ' new '}translation bundle :${originBlueprintId}:${language})`) } else { - logger.error(`Unable to store translation bundle ([${_id}]:${namespace}:${language})`, { + logger.error(`Unable to store translation bundle ([${_id}]:${originBlueprintId}:${language})`, { error: err, }) } @@ -51,10 +58,14 @@ export function upsertBundles(bundles: BlueprintTranslationsbundle[], parentBlue } } -function getExistingId(namespace: string | undefined, language: string): TranslationsBundleId | null { - const bundle = TranslationsBundleCollection.findOne({ namespace, language }) +function createBundleId(blueprintId: BlueprintId, language: string): TranslationsBundleId { + return protectString(getHash(`TranslationsBundle${blueprintId}${language}`)) +} + +function getExistingId(originBlueprintId: BlueprintId, language: string): TranslationsBundleId | null { + const bundle = TranslationsBundleCollection.findOne({ originBlueprintId, language }) - return bundle ? bundle._id : null + return bundle?._id ?? null } function fetchAvailableBundles(dbCursor: Mongocursor<{ _id: TranslationsBundleId } & DBTranslationsBundle>) { @@ -62,6 +73,13 @@ function fetchAvailableBundles(dbCursor: Mongocursor<{ _id: TranslationsBundleId return stuff.map(({ namespace, language }) => ({ namespace, language })) } +/** + * Retrieves a bundle from the database + * + * @param bundleId the id of the bundle to retrieve + * @returns the bundle with the given id + * @throws if there is no bundle with the given id + */ export function getBundle(bundleId: TranslationsBundleId) { const bundle = TranslationsBundleCollection.findOne(bundleId) if (!bundle) { From 181b31ca608845630646cb1270e70d267d451b60 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 2 Feb 2021 17:31:19 +0100 Subject: [PATCH 55/58] fix: remove debug log statements used under developement, commented parts of the code that aren't immediately intuitive --- meteor/client/ui/i18n.ts | 9 +++------ meteor/server/api/blueprints/api.ts | 17 +++++++++++------ meteor/server/api/translationsBundles.ts | 19 +------------------ 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 359eeae061..67fdad3d09 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -95,9 +95,8 @@ class I18nContainer extends WithManagedTracker { this.subscribe(PubSub.translationsBundles, {}) this.autorun(() => { - console.debug('ManagedTracker autorun...') const bundlesInfo = TranslationsBundles.find().fetch() as Omit[] - console.debug(`Got ${bundlesInfo.length} bundles from database`) + Promise.allSettled( bundlesInfo.map((bundleMetadata) => new Promise((resolve) => { @@ -128,17 +127,15 @@ class I18nContainer extends WithManagedTracker { true, true ) - console.debug('i18instance updated', { - bundle: { lang: bundle.language, ns: bundle.namespace }, - }) }) .catch((reason) => { console.error(`Failed to fetch translations bundle "${bundleMetadata._id}": `, reason) }) ) - ).then(() => console.debug(`Finished updating ${bundlesInfo.length} translation bundles`)) + ) }) } + // return key until real translator comes online i18nTranslator(key, ...args) { console.debug('i18nTranslator placeholder called', { key, args }) diff --git a/meteor/server/api/blueprints/api.ts b/meteor/server/api/blueprints/api.ts index 12794adeed..ddd9ba1e7f 100644 --- a/meteor/server/api/blueprints/api.ts +++ b/meteor/server/api/blueprints/api.ts @@ -183,12 +183,17 @@ export function innerUploadBlueprint( newBlueprint.studioConfigManifest = blueprintManifest.studioConfigManifest } - // extract and store translations bundled with the manifest if any - logger.debug(`blueprintManifest for ${newBlueprint.name} translations`, { - translations: (blueprintManifest as any).translations, - type: typeof (blueprintManifest as any).translations, - }) - if ((blueprintManifest as any).translations) { + // check for translations on the manifest and store them if they exist + if ( + 'translations' in blueprintManifest && + (blueprintManifest.blueprintType === BlueprintManifestType.SHOWSTYLE || + blueprintManifest.blueprintType === BlueprintManifestType.STUDIO) + ) { + // Because the translations is bundled as stringified JSON and that string has already been + // converted back to object form together with the rest of the manifest at this point + // the casting is actually necessary. + // Note that the type has to be string in the manifest interfaces to allow attaching the + // stringified JSON in the first place. const translations = (blueprintManifest as any).translations as TranslationsBundle[] upsertBundles(translations, newBlueprint.blueprintId) } diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index 33dfa3e69f..0df740ce81 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -11,7 +11,6 @@ import { TranslationsBundleType, } from '@sofie-automation/blueprints-integration' import { getHash, protectString, unprotectString } from '../../lib/lib' -import { logger } from '../logging' import { BlueprintId } from '../../lib/collections/Blueprints' import { Mongocursor } from '../../lib/typings/meteor' @@ -42,18 +41,7 @@ export function upsertBundles(bundles: BlueprintTranslationsbundle[], originBlue data: fromI18NextData(data), hash: getHash(JSON.stringify(data)), }, - { multi: false }, - (err: Error, numberAffected: number) => { - if (!err && numberAffected) { - logger.info(`Stored ${_id ? '' : ' new '}translation bundle :${originBlueprintId}:${language})`) - } else { - logger.error(`Unable to store translation bundle ([${_id}]:${originBlueprintId}:${language})`, { - error: err, - }) - } - const dbCursor = TranslationsBundleCollection.find({}) - logger.debug(`${dbCursor.count()} bundles in database:`, { bundles: fetchAvailableBundles(dbCursor) }) - } + { multi: false } ) } } @@ -68,11 +56,6 @@ function getExistingId(originBlueprintId: BlueprintId, language: string): Transl return bundle?._id ?? null } -function fetchAvailableBundles(dbCursor: Mongocursor<{ _id: TranslationsBundleId } & DBTranslationsBundle>) { - const stuff = dbCursor.fetch() - return stuff.map(({ namespace, language }) => ({ namespace, language })) -} - /** * Retrieves a bundle from the database * From cbf0506f10adf36f6e33cea8e4dd10d038583289 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Tue, 2 Feb 2021 17:39:28 +0100 Subject: [PATCH 56/58] chore: fix typo in user facing error message --- meteor/lib/api/rundown.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/lib/api/rundown.ts b/meteor/lib/api/rundown.ts index ba46b08917..a2f47e9618 100644 --- a/meteor/lib/api/rundown.ts +++ b/meteor/lib/api/rundown.ts @@ -89,7 +89,7 @@ function handleRundownContextError(rundown: Rundown, errorInformMessage: string type: NoteType.ERROR, message: { key: `${errorInformMessage || - 'Something went wrong when processing data this rundown.'} Error message: {{error}}`, + 'Something went wrong when processing data for this rundown.'} Error message: {{error}}`, args: { error: `${error || 'N/A'}`, }, From 558c4b377f04468abc30536f84af9ee4068d4160 Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 3 Feb 2021 16:38:40 +0100 Subject: [PATCH 57/58] fix: minor improvements after code review --- meteor/server/api/translationsBundles.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/meteor/server/api/translationsBundles.ts b/meteor/server/api/translationsBundles.ts index 0df740ce81..2aeab8fc1b 100644 --- a/meteor/server/api/translationsBundles.ts +++ b/meteor/server/api/translationsBundles.ts @@ -12,7 +12,6 @@ import { } from '@sofie-automation/blueprints-integration' import { getHash, protectString, unprotectString } from '../../lib/lib' import { BlueprintId } from '../../lib/collections/Blueprints' -import { Mongocursor } from '../../lib/typings/meteor' /** * Insert or update translation bundles in the database. @@ -28,7 +27,9 @@ export function upsertBundles(bundles: BlueprintTranslationsbundle[], originBlue throw new Error(`Unknown bundle type ${type}`) } - const _id = getExistingId(originBlueprintId, language) || createBundleId(originBlueprintId, language) + // doesn't matter if it's a new or existing bundle, the id will be the same with the same + // originating blueprint and language + const _id = createBundleId(originBlueprintId, language) TranslationsBundleCollection.upsert( _id, @@ -46,16 +47,19 @@ export function upsertBundles(bundles: BlueprintTranslationsbundle[], originBlue } } +/** + * Creates an id for a bundle based on its originating blueprint and language (which are + * guaranteed to be unique, as there is only one set of translations per language per blueprint). + * The id hash is guaranteed to be the same for every call with equal input, meaning it can be used + * to find a previously generated id as well as generating new ids. + * + * @param blueprintId the id of the blueprint the translations were bundled with + * @param language the language the bundle contains translations for + */ function createBundleId(blueprintId: BlueprintId, language: string): TranslationsBundleId { return protectString(getHash(`TranslationsBundle${blueprintId}${language}`)) } -function getExistingId(originBlueprintId: BlueprintId, language: string): TranslationsBundleId | null { - const bundle = TranslationsBundleCollection.findOne({ originBlueprintId, language }) - - return bundle?._id ?? null -} - /** * Retrieves a bundle from the database * @@ -63,7 +67,7 @@ function getExistingId(originBlueprintId: BlueprintId, language: string): Transl * @returns the bundle with the given id * @throws if there is no bundle with the given id */ -export function getBundle(bundleId: TranslationsBundleId) { +export function getBundle(bundleId: TranslationsBundleId): DBTranslationsBundle { const bundle = TranslationsBundleCollection.findOne(bundleId) if (!bundle) { throw new Meteor.Error(404, `Bundle "${bundleId}" not found`) From 96a8d75aa7e2cb5327b77c6bb7d47727feeab63b Mon Sep 17 00:00:00 2001 From: Ola Christian Gundelsby Date: Wed, 3 Feb 2021 18:24:14 +0100 Subject: [PATCH 58/58] fix: move getTranslationBundle method from userActions to systems api --- meteor/client/ui/i18n.ts | 2 +- meteor/lib/api/system.ts | 6 +++++- meteor/lib/api/userActions.ts | 2 -- meteor/server/api/system.ts | 17 +++++++++++++++-- meteor/server/api/userActions.ts | 15 ++------------- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/meteor/client/ui/i18n.ts b/meteor/client/ui/i18n.ts index 67fdad3d09..b6d3b8a36e 100644 --- a/meteor/client/ui/i18n.ts +++ b/meteor/client/ui/i18n.ts @@ -56,7 +56,7 @@ function toI18NextData(translations: Translation[]): I18NextData { function getAndCacheTranslationBundle(bundleId: TranslationsBundleId) { return new Promise((resolve, reject) => - MeteorCall.userAction.getTranslationBundle(bundleId).then( + MeteorCall.system.getTranslationBundle(bundleId).then( (response) => { if (ClientAPI.isClientResponseSuccess(response) && response.result) { localStorage.setItem(`i18n.translationBundles.${bundleId}`, JSON.stringify(response.result)) diff --git a/meteor/lib/api/system.ts b/meteor/lib/api/system.ts index e7f1a048ff..9983b38451 100644 --- a/meteor/lib/api/system.ts +++ b/meteor/lib/api/system.ts @@ -1,4 +1,5 @@ -import { MethodContext } from './methods' +import { TranslationsBundle, TranslationsBundleId } from '../collections/TranslationsBundles' +import { ClientAPI } from './client' export interface CollectionCleanupResult { collectionName: string @@ -23,10 +24,13 @@ export interface SystemAPI { cleanupOldData(actuallyRemoveOldData: boolean): Promise doSystemBenchmark(): Promise + + getTranslationBundle(bundleId: TranslationsBundleId): Promise> } export enum SystemAPIMethods { 'cleanupIndexes' = 'system.cleanupIndexes', 'cleanupOldData' = 'system.cleanupOldData', 'doSystemBenchmark' = 'system.doSystemBenchmark', + 'getTranslationBundle' = 'system.getTranslationBundle', } diff --git a/meteor/lib/api/userActions.ts b/meteor/lib/api/userActions.ts index d9f176e63c..04165570ce 100644 --- a/meteor/lib/api/userActions.ts +++ b/meteor/lib/api/userActions.ts @@ -17,7 +17,6 @@ import { IngestAdlib, ActionUserData } from '@sofie-automation/blueprints-integr import { BucketAdLib } from '../collections/BucketAdlibs' import { AdLibActionId, AdLibActionCommon } from '../collections/AdLibActions' import { BucketAdLibAction } from '../collections/BucketAdlibActions' -import { TranslationsBundle, TranslationsBundleId } from '../collections/TranslationsBundles' export interface NewUserActionAPI extends MethodContext { take(userEvent: string, rundownPlaylistId: RundownPlaylistId): Promise> @@ -161,7 +160,6 @@ export interface NewUserActionAPI extends MethodContext { restartCore(userEvent: string, token: string): Promise> guiFocused(userEvent: string, viewInfo?: any[]): Promise> guiBlurred(userEvent: string, viewInfo?: any[]): Promise> - getTranslationBundle(bundleId: TranslationsBundleId): Promise> bucketsRemoveBucket(userEvent: string, id: BucketId): Promise> bucketsModifyBucket( userEvent: string, diff --git a/meteor/server/api/system.ts b/meteor/server/api/system.ts index d355e0099f..522602d8ac 100644 --- a/meteor/server/api/system.ts +++ b/meteor/server/api/system.ts @@ -36,7 +36,7 @@ import { Organizations, OrganizationId } from '../../lib/collections/Organizatio import { PartInstances } from '../../lib/collections/PartInstances' import { Parts } from '../../lib/collections/Parts' import { PeripheralDeviceCommands } from '../../lib/collections/PeripheralDeviceCommands' -import { PeripheralDevices, PeripheralDeviceId, PeripheralDevice } from '../../lib/collections/PeripheralDevices' +import { PeripheralDevices, PeripheralDeviceId } from '../../lib/collections/PeripheralDevices' import { Pieces } from '../../lib/collections/Pieces' import { RundownBaselineAdLibActions } from '../../lib/collections/RundownBaselineAdLibActions' import { RundownBaselineAdLibPieces } from '../../lib/collections/RundownBaselineAdLibPieces' @@ -54,7 +54,10 @@ import { UserActionsLog } from '../../lib/collections/UserActionsLog' import { getActiveRundownPlaylistsInStudio } from './playout/studio' import { PieceInstances } from '../../lib/collections/PieceInstances' import { createMongoCollection } from '../../lib/collections/lib' -import { PeripheralDeviceAPI } from '../../lib/api/peripheralDevice' +import { getBundle as getTranslationBundleInner } from './translationsBundles' +import { TranslationsBundle, TranslationsBundleId } from '../../lib/collections/TranslationsBundles' +import { OrganizationContentWriteAccess } from '../security/organization' +import { ClientAPI } from '../../lib/api/client' function setupIndexes(removeOldIndexes: boolean = false): IndexSpecification[] { // Note: This function should NOT run on Meteor.startup, due to getCollectionIndexes failing if run before indexes have been created. @@ -735,6 +738,13 @@ CPU JSON stringifying: ${avg.cpuStringifying} ms (${comparison.cpuStringif } } +function getTranslationBundle(context: MethodContext, bundleId: TranslationsBundleId) { + check(bundleId, String) + + OrganizationContentWriteAccess.anyContent(context) + return ClientAPI.responseSuccess(getTranslationBundleInner(bundleId)) +} + class SystemAPIClass extends MethodContextAPI implements SystemAPI { cleanupIndexes(actuallyRemoveOldIndexes: boolean) { return makePromise(() => cleanupIndexes(this, actuallyRemoveOldIndexes)) @@ -745,5 +755,8 @@ class SystemAPIClass extends MethodContextAPI implements SystemAPI { async doSystemBenchmark(runCount: number = 1) { return doSystemBenchmark(this, runCount) } + getTranslationBundle(bundleId: TranslationsBundleId): Promise> { + return makePromise(() => getTranslationBundle(this, bundleId)) + } } registerClassToMeteorMethods(SystemAPIMethods, SystemAPIClass, false) diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 3c58f547f5..0000eca87a 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -3,7 +3,7 @@ import { check, Match } from '../../lib/check' import { Meteor } from 'meteor/meteor' import { ClientAPI } from '../../lib/api/client' import { getCurrentTime, getHash, makePromise } from '../../lib/lib' -import { Rundowns, RundownHoldState, RundownId, Rundown } from '../../lib/collections/Rundowns' +import { Rundowns, RundownHoldState, RundownId } from '../../lib/collections/Rundowns' import { Parts, Part, PartId } from '../../lib/collections/Parts' import { logger } from '../logging' import { ServerPlayoutAPI } from './playout/playout' @@ -21,7 +21,7 @@ import { IngestDataCache, IngestCacheType } from '../../lib/collections/IngestDa import { MOSDeviceActions } from './ingest/mosDevice/actions' import { getActiveRundownPlaylistsInStudio } from './playout/studio' import { IngestActions } from './ingest/actions' -import { RundownPlaylists, RundownPlaylistId, RundownPlaylist } from '../../lib/collections/RundownPlaylists' +import { RundownPlaylists, RundownPlaylistId } from '../../lib/collections/RundownPlaylists' import { PartInstances, PartInstanceId } from '../../lib/collections/PartInstances' import { PieceInstances, @@ -50,8 +50,6 @@ import { profiler } from './profiler' import { AdLibActionId, AdLibActionCommon } from '../../lib/collections/AdLibActions' import { BucketAdLibAction } from '../../lib/collections/BucketAdlibActions' import { checkAccessAndGetPlaylist, checkAccessAndGetRundown } from './lib' -import { TranslationsBundle, TranslationsBundleId } from '../../lib/collections/TranslationsBundles' -import { getBundle as getTranslationBundleInner } from './translationsBundles' let MINIMUM_TAKE_SPAN = 1000 export function setMinimumTakeSpan(span: number) { @@ -595,12 +593,6 @@ export function mediaAbortAllWorkflows(context: MethodContext) { const access = OrganizationContentWriteAccess.anyContent(context) return ClientAPI.responseSuccess(MediaManagerAPI.abortAllWorkflows(context, access.organizationId)) } -export function getTranslationBundle(context: MethodContext, bundleId: TranslationsBundleId) { - check(bundleId, String) - - const access = OrganizationContentWriteAccess.anyContent(context) - return ClientAPI.responseSuccess(getTranslationBundleInner(bundleId)) -} export function bucketsRemoveBucket(context: MethodContext, id: BucketId) { check(id, String) @@ -1066,9 +1058,6 @@ class ServerUserActionAPI extends MethodContextAPI implements NewUserActionAPI { guiBlurred(_userEvent: string, _viewInfo: any[]) { return traceAction('userAction.noop', noop, this) } - getTranslationBundle(bundleId: TranslationsBundleId): Promise> { - return makePromise(() => getTranslationBundle(this, bundleId)) - } bucketsRemoveBucket(_userEvent: string, id: BucketId) { return traceAction(UserActionAPIMethods.bucketsRemoveBucket, bucketsRemoveBucket, this, id) }