diff --git a/app/javascript/internal/__mocks__/mutationObserver.js b/app/javascript/internal/__mocks__/mutationObserver.js
new file mode 100644
index 000000000..bd9e91cba
--- /dev/null
+++ b/app/javascript/internal/__mocks__/mutationObserver.js
@@ -0,0 +1,23 @@
+// Sourced from https://github.com/stimulusjs/stimulus/issues/34
+// and https://shime.sh/testing-stimulus
+
+// Setup MutationObserver shim since jsdom doesn't
+// support it out of the box.
+
+import { readFileSync } from 'fs';
+import { resolve } from 'path';
+
+const shim = readFileSync(
+ resolve(
+ 'node_modules',
+ 'mutationobserver-shim',
+ 'dist',
+ 'mutationobserver.min.js',
+ ),
+ { encoding: 'utf-8' },
+);
+
+const script = window.document.createElement('script');
+script.textContent = shim;
+
+window.document.body.appendChild(script);
diff --git a/app/javascript/internal/__tests__/controllers/article_controller.test.js b/app/javascript/internal/__tests__/controllers/article_controller.test.js
new file mode 100644
index 000000000..e7011c9be
--- /dev/null
+++ b/app/javascript/internal/__tests__/controllers/article_controller.test.js
@@ -0,0 +1,61 @@
+import { Application } from 'stimulus';
+import ArticleController from '../../controllers/article_controller';
+import '../../__mocks__/mutationObserver';
+
+describe('ArticleController', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `
+
+
+
+
+
`;
+
+ const application = Application.start();
+ application.register('article', ArticleController);
+ });
+
+ // Unix timestamp, one hour ago
+ const initialValue = Math.round((Date.now() - 3600000) / 1000);
+
+ describe('#increaseFeaturedNumber', () => {
+ it('increases the featured number input', () => {
+ const button = document.querySelectorAll('button')[0];
+ const input = document.querySelector(
+ "[data-target='article.featuredNumber']",
+ );
+
+ input.value = initialValue;
+ button.click();
+
+ expect(parseInt(input.value, 10)).toBeGreaterThan(initialValue);
+ });
+ });
+
+ describe('#decreaseFeaturedNumber', () => {
+ it('increases the featured number input', () => {
+ const button = document.querySelectorAll('button')[1];
+ const input = document.querySelector(
+ "[data-target='article.featuredNumber']",
+ );
+
+ input.value = initialValue;
+ button.click();
+
+ expect(parseInt(input.value, 10)).toBeLessThan(initialValue);
+ });
+ });
+
+ describe('#highlightElement', () => {
+ it('adds a class to the controller element', () => {
+ const button = document.querySelectorAll('button')[2];
+ const element = document.querySelector("[data-controller='article']");
+
+ button.click();
+
+ expect(
+ element.classList.contains('highlighted-bg', 'highlighted-border'),
+ ).toBe(true);
+ });
+ });
+});
diff --git a/app/javascript/internal/__tests__/controllers/buffer_controller.test.js b/app/javascript/internal/__tests__/controllers/buffer_controller.test.js
new file mode 100644
index 000000000..0fef95860
--- /dev/null
+++ b/app/javascript/internal/__tests__/controllers/buffer_controller.test.js
@@ -0,0 +1,52 @@
+import { Application } from 'stimulus';
+import BufferController from '../../controllers/buffer_controller';
+import '../../__mocks__/mutationObserver';
+
+describe('BufferController', () => {
+ beforeEach(() => {
+ document.body.innerHTML = `
+
+
+
+
+ `;
+
+ const application = Application.start();
+ application.register('buffer', BufferController);
+ });
+
+ describe('#tagBufferUpdateConfirmed', () => {
+ it('adds a badge to the header', () => {
+ const button = document.querySelectorAll('button')[0];
+ const header = document.querySelector('h2');
+
+ button.click();
+
+ expect(header.firstChild.textContent).toMatch(/Confirm/);
+ });
+ });
+
+ describe('#tagBufferUpdateDismissed', () => {
+ it('adds a badge to the header', () => {
+ const button = document.querySelectorAll('button')[1];
+ const header = document.querySelector('h2');
+
+ button.click();
+
+ expect(header.firstChild.textContent).toMatch(/Dismiss/);
+ });
+ });
+
+ describe('#highlightElement', () => {
+ it('adds a class to the controller element', () => {
+ const button = document.querySelectorAll('button')[2];
+ const element = document.querySelector("[data-controller='buffer']");
+
+ button.click();
+
+ expect(
+ element.classList.contains('highlighted-bg', 'highlighted-border'),
+ ).toBe(true);
+ });
+ });
+});
diff --git a/app/javascript/internal/controllers/article_controller.js b/app/javascript/internal/controllers/article_controller.js
new file mode 100644
index 000000000..9d515db86
--- /dev/null
+++ b/app/javascript/internal/controllers/article_controller.js
@@ -0,0 +1,32 @@
+import { Controller } from 'stimulus';
+
+export default class ArticleController extends Controller {
+ static targets = ['featuredNumber'];
+
+ increaseFeaturedNumber() {
+ // Increases the article's chances of being seen
+ const seconds = new Date().getTime() / 1000;
+ this.featuredNumberTarget.value = Math.round(seconds);
+ }
+
+ decreaseFeaturedNumber() {
+ // Decreases the article's chances of being seen
+ const seconds = new Date().getTime() / 1080;
+ this.featuredNumberTarget.value = Math.round(seconds);
+ }
+
+ highlightElement() {
+ this.element.classList.add('highlighted-bg', 'highlighted-border');
+ setTimeout(() => {
+ this.element.classList.remove('highlighted-bg');
+ }, 350);
+ }
+
+ get articleId() {
+ return parseInt(this.data.get('id'), 10);
+ }
+
+ set articleId(value) {
+ this.data.set('id', value);
+ }
+}
diff --git a/app/javascript/internal/controllers/buffer_controller.js b/app/javascript/internal/controllers/buffer_controller.js
new file mode 100644
index 000000000..47b7c9323
--- /dev/null
+++ b/app/javascript/internal/controllers/buffer_controller.js
@@ -0,0 +1,30 @@
+import { Controller } from 'stimulus';
+
+export default class BufferController extends Controller {
+ static targets = ['header'];
+
+ tagBufferUpdateConfirmed() {
+ this.headerTarget.innerHTML +=
+ 'Confirm';
+ }
+
+ tagBufferUpdateDismissed() {
+ this.headerTarget.innerHTML +=
+ 'Dismiss';
+ }
+
+ highlightElement() {
+ this.element.classList.add('highlighted-bg', 'highlighted-border');
+ setTimeout(() => {
+ this.element.classList.remove('highlighted-bg');
+ }, 350);
+ }
+
+ get bufferUpdateId() {
+ return parseInt(this.data.get('id'), 10);
+ }
+
+ set bufferUpdateId(value) {
+ this.data.set('id', value);
+ }
+}
diff --git a/app/javascript/packs/internal.js b/app/javascript/packs/internal.js
new file mode 100644
index 000000000..e149f445e
--- /dev/null
+++ b/app/javascript/packs/internal.js
@@ -0,0 +1,6 @@
+import { Application } from 'stimulus';
+import { definitionsFromContext } from 'stimulus/webpack-helpers';
+
+const application = Application.start();
+const context = require.context('internal/controllers', true, /.js$/);
+application.load(definitionsFromContext(context));
diff --git a/app/views/internal/articles/_article_script.html.erb b/app/views/internal/articles/_image_upload_script.html.erb
similarity index 55%
rename from app/views/internal/articles/_article_script.html.erb
rename to app/views/internal/articles/_image_upload_script.html.erb
index 76ea1f3bc..09e215312 100644
--- a/app/views/internal/articles/_article_script.html.erb
+++ b/app/views/internal/articles/_image_upload_script.html.erb
@@ -1,50 +1,5 @@
diff --git a/package.json b/package.json
index b94c36f10..8c1bd0640 100644
--- a/package.json
+++ b/package.json
@@ -79,6 +79,7 @@
"jest-fetch-mock": "^3.0.1",
"jsdom": "^15.2.1",
"lint-staged": "^10.0.0",
+ "mutationobserver-shim": "^0.3.3",
"preact-render-spy": "1.3.0",
"preact-render-to-json": "^3.6.6",
"prettier": "^1.19.1",
@@ -105,6 +106,7 @@
"preact-textarea-autosize": "^4.0.7",
"prop-types": "^15.7.2",
"pusher-js": "^5.0.3",
+ "stimulus": "^1.1.1",
"twilio-video": "^2.0.0",
"web-share-wrapper": "^0.2.1"
},
diff --git a/yarn.lock b/yarn.lock
index ecabd2193..835a6a456 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -290,6 +290,30 @@
dependencies:
any-observable "^0.3.0"
+"@stimulus/core@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@stimulus/core/-/core-1.1.1.tgz#42b0cfe5b73ca492f41de64b77a03980bae92c82"
+ integrity sha512-PVJv7IpuQx0MVPCBblXc6O2zbCmU8dlxXNH4bC9KK6LsvGaE+PCXXrXQfXUwAsse1/CmRu/iQG7Ov58himjiGg==
+ dependencies:
+ "@stimulus/mutation-observers" "^1.1.1"
+
+"@stimulus/multimap@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@stimulus/multimap/-/multimap-1.1.1.tgz#b95e3fd607345ab36e5d5b55486ee1a12d56b331"
+ integrity sha512-26R1fI3a8uUj0WlMmta4qcfIQGlagegdP4PTz6lz852q/dXlG6r+uPS/bx+H8GtfyS+OOXVr3SkZ0Zg0iRqRfQ==
+
+"@stimulus/mutation-observers@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@stimulus/mutation-observers/-/mutation-observers-1.1.1.tgz#0f6c6f081308427fed2a26360dda0c173b79cfc0"
+ integrity sha512-/zCnnw1KJlWO2mrx0yxYaRFZWMGnDMdOgSnI4hxDLxdWVuL2HMROU8FpHWVBLjKY3T9A+lGkcrmPGDHF3pfS9w==
+ dependencies:
+ "@stimulus/multimap" "^1.1.1"
+
+"@stimulus/webpack-helpers@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@stimulus/webpack-helpers/-/webpack-helpers-1.1.1.tgz#eff60cd4e58b921d1a2764dc5215f5141510f2c2"
+ integrity sha512-XOkqSw53N9072FLHvpLM25PIwy+ndkSSbnTtjKuyzsv8K5yfkFB2rv68jU1pzqYa9FZLcvZWP4yazC0V38dx9A==
+
"@storybook/addon-actions@3.4.12":
version "3.4.12"
resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.4.12.tgz#ff6cbaf563c3cb5d648d6a35f66cfa50ced49bf4"
@@ -8297,6 +8321,11 @@ multicast-dns@^6.0.1:
dns-packet "^1.3.1"
thunky "^1.0.2"
+mutationobserver-shim@^0.3.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#65869630bc89d7bf8c9cd9cb82188cd955aacd2b"
+ integrity sha512-gciOLNN8Vsf7YzcqRjKzlAJ6y7e+B86u7i3KXes0xfxx/nfLmozlW1Vn+Sc9x3tPIePFgc1AeIFhtRgkqTjzDQ==
+
mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
@@ -12071,6 +12100,14 @@ stickyfill@^1.1.1:
resolved "https://registry.yarnpkg.com/stickyfill/-/stickyfill-1.1.1.tgz#39413fee9d025c74a7e59ceecb23784cc0f17f02"
integrity sha1-OUE/7p0CXHSn5ZzuyyN4TMDxfwI=
+stimulus@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stimulus/-/stimulus-1.1.1.tgz#53c2fded6849e7b85eed3ed8dd76e33abd74bec5"
+ integrity sha512-R0mBqKp48YnRDZOxZ8hiOH4Ilph3Yj78CIFTBkCwyHs4iGCpe7xlEdQ7cjIxb+7qVCSxFKgxO+mAQbsNgt/5XQ==
+ dependencies:
+ "@stimulus/core" "^1.1.1"
+ "@stimulus/webpack-helpers" "^1.1.1"
+
store2@^2.7.1:
version "2.7.1"
resolved "https://registry.yarnpkg.com/store2/-/store2-2.7.1.tgz#22070b7dc04748a792fc6912a58ab99d3a21d788"