Add workflow menu button to thread lists
Bug: twpowertools:74
Change-Id: I703950394d674c2084278bf9e876014d08fa5cfb
diff --git a/src/contentScripts/communityConsole/batchLock.js b/src/contentScripts/communityConsole/batchLock.js
index 86a7057..87edc6d 100644
--- a/src/contentScripts/communityConsole/batchLock.js
+++ b/src/contentScripts/communityConsole/batchLock.js
@@ -1,19 +1,12 @@
import {isOptionEnabled} from '../../common/optionsUtils.js';
-import {addButtonToThreadListActions, removeChildNodes} from './utils/common.js';
+import {addButtonToThreadListActions, removeChildNodes, shouldAddBtnToActionBar} from './utils/common.js';
+
+const lockDebugId = 'twpt-batch-lock';
export var batchLock = {
- nodeIsReadToggleBtn(node) {
- return ('tagName' in node) && node.tagName == 'MATERIAL-BUTTON' &&
- node.getAttribute('debugid') !== null &&
- (node.getAttribute('debugid') == 'mark-read-button' ||
- node.getAttribute('debugid') == 'mark-unread-button') &&
- ('parentNode' in node) && node.parentNode !== null &&
- ('parentNode' in node.parentNode) &&
- node.parentNode.querySelector('[debugid="twpt-lock"]') === null &&
- node.parentNode.parentNode !== null &&
- ('tagName' in node.parentNode.parentNode) &&
- node.parentNode.parentNode.tagName == 'EC-BULK-ACTIONS';
+ shouldAddButton(node) {
+ return shouldAddBtnToActionBar(lockDebugId, node);
},
createDialog() {
var modal = document.querySelector('.pane[pane-id="default-1"]');
@@ -119,7 +112,7 @@
if (isEnabled) {
let tooltip = chrome.i18n.getMessage('inject_lockbtn');
let btn = addButtonToThreadListActions(
- readToggle, 'lock', 'twpt-lock', tooltip);
+ readToggle, 'lock', lockDebugId, tooltip);
btn.addEventListener('click', () => {
this.createDialog();
});
diff --git a/src/contentScripts/communityConsole/main.js b/src/contentScripts/communityConsole/main.js
index cc55aa4..870f1c4 100644
--- a/src/contentScripts/communityConsole/main.js
+++ b/src/contentScripts/communityConsole/main.js
@@ -9,9 +9,10 @@
import {applyDragAndDropFixIfEnabled} from './dragAndDropFix.js';
// #!endif
import {unifiedProfilesFix} from './unifiedProfiles.js';
+import Workflows from './workflows/workflows.js';
var mutationObserver, intersectionObserver, intersectionOptions, options,
- avatars;
+ avatars, workflows;
const watchedNodesSelectors = [
// App container (used to set up the intersection observer and inject the dark
@@ -112,11 +113,14 @@
}
// #!endif
- // Inject the batch lock button in the thread list if the option is
- // currently enabled.
- if (batchLock.nodeIsReadToggleBtn(node)) {
+ // Inject the batch lock and workflow buttons in the thread list if the
+ // corresponding options are currently enabled.
+ // The order is the inverse because the first one will be shown last.
+ if (batchLock.shouldAddButton(node))
batchLock.addButtonIfEnabled(node);
- }
+
+ if (workflows.shouldAddThreadListBtn(node))
+ workflows.addThreadListBtnIfEnabled(node);
// Inject avatar links to threads in the thread list. injectIfEnabled is
// responsible of determining whether it should run or not depending on its
@@ -180,6 +184,7 @@
// Initialize classes needed by the mutation observer
avatars = new AvatarsHandler();
+ workflows = new Workflows();
// autoRefresh is initialized in start.js
diff --git a/src/contentScripts/communityConsole/utils/common.js b/src/contentScripts/communityConsole/utils/common.js
index 648fae2..68fb736 100644
--- a/src/contentScripts/communityConsole/utils/common.js
+++ b/src/contentScripts/communityConsole/utils/common.js
@@ -34,7 +34,8 @@
// Adds a button to the thread list actions bar next to the button given by
// |originalBtn|. The button will have icon |icon|, when hovered it will display
// |tooltip|, and will have a debugid attribute with value |debugId|.
-export function addButtonToThreadListActions(originalBtn, icon, debugId, tooltip) {
+export function addButtonToThreadListActions(
+ originalBtn, icon, debugId, tooltip) {
let clone = originalBtn.cloneNode(true);
clone.setAttribute('debugid', debugId);
clone.classList.add('TWPT-btn--with-badge');
@@ -59,3 +60,15 @@
return clone;
}
+
+// Returns true if |node| is the "mark as read/unread" button, the parent of the
+// parent of |node| is the actions bar of the thread list, and the button with
+// debugid |debugid| is NOT part of the actions bar.
+export function shouldAddBtnToActionBar(debugid, node) {
+ return node?.tagName == 'MATERIAL-BUTTON' &&
+ (node.getAttribute?.('debugid') == 'mark-read-button' ||
+ node.getAttribute?.('debugid') == 'mark-unread-button') &&
+ node.getAttribute?.('debugid') !== null &&
+ node.parentNode?.querySelector('[debugid="' + debugid + '"]') === null &&
+ node.parentNode?.parentNode?.tagName == 'EC-BULK-ACTIONS';
+}
diff --git a/src/contentScripts/communityConsole/workflows/components/Overlay.vue b/src/contentScripts/communityConsole/workflows/components/Overlay.vue
new file mode 100644
index 0000000..304b362
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/components/Overlay.vue
@@ -0,0 +1,29 @@
+<script>
+import WfMenu from './WfMenu.vue';
+
+export default {
+ components: {
+ WfMenu,
+ },
+ data() {
+ return {
+ shown: false,
+ position: [0, 0],
+ // TODO: Get real data.
+ workflows: [
+ {name: 'Move to accounts'},
+ {name: 'Mark as spam w/ message'},
+ ],
+ };
+ },
+ methods: {
+ startWorkflow(e) {
+ console.log(e);
+ }
+ },
+}
+</script>
+
+<template>
+ <wf-menu v-model="shown" :position="position" :workflows="workflows" @select="startWorkflow" />
+</template>
diff --git a/src/contentScripts/communityConsole/workflows/components/WfMenu.vue b/src/contentScripts/communityConsole/workflows/components/WfMenu.vue
new file mode 100644
index 0000000..88528ea
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/components/WfMenu.vue
@@ -0,0 +1,39 @@
+<script>
+import {Corner} from '@material/menu-surface/constants.js';
+
+export default {
+ components: {},
+ props: {
+ modelValue: Boolean,
+ position: Array,
+ workflows: Array,
+ },
+ data() {
+ return {
+ corner: Corner.TOP_RIGHT,
+ };
+ },
+ emits: [
+ 'update:modelValue',
+ 'select',
+ ],
+}
+</script>
+
+<template>
+ <mcw-menu :model-value="modelValue" fixed :anchor-corner="corner"
+ :style="{ left: 'unset', right: 'calc(100% - ' + position[0] + 'px)', top: position[1] + 'px' }"
+ @update:model-value="$emit('update:modelValue', $event)" @select="$emit('select', $event)">
+ <mcw-list-item v-for="wf in workflows">{{ wf.name }}</mcw-list-item>
+ </mcw-menu>
+</template>
+
+<style scoped>
+.mdc-list-item {
+ /* These styles mimic the Community Console style. */
+ font-family: 'Google Sans Text', 'Noto', sans-serif;
+ font-size: 14px;
+ font-weight: 400;
+ height: 40px!important;
+}
+</style>
diff --git a/src/contentScripts/communityConsole/workflows/vma.js b/src/contentScripts/communityConsole/workflows/vma.js
new file mode 100644
index 0000000..89d291c
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/vma.js
@@ -0,0 +1,9 @@
+// We just import the components needed.
+import {list, menu} from 'vue-material-adapter';
+
+export default {
+ install(vm) {
+ vm.use(list);
+ vm.use(menu);
+ },
+}
diff --git a/src/contentScripts/communityConsole/workflows/workflows.js b/src/contentScripts/communityConsole/workflows/workflows.js
new file mode 100644
index 0000000..dc542fa
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/workflows.js
@@ -0,0 +1,53 @@
+import {createApp} from 'vue';
+
+import {isOptionEnabled} from '../../../common/optionsUtils.js';
+
+import {addButtonToThreadListActions, shouldAddBtnToActionBar} from './../utils/common.js';
+import Overlay from './components/Overlay.vue';
+import VueMaterialAdapter from './vma.js';
+
+const wfDebugId = 'twpt-workflows';
+
+export default class Workflows {
+ constructor() {
+ this.overlayApp = null;
+ this.overlayVm = null;
+ }
+
+ createOverlay() {
+ let menuEl = document.createElement('div');
+ document.body.appendChild(menuEl);
+
+ this.overlayApp = createApp(Overlay);
+ this.overlayApp.use(VueMaterialAdapter);
+ this.overlayVm = this.overlayApp.mount(menuEl);
+ }
+
+ switchMenu(menuBtn) {
+ if (this.overlayApp === null) this.createOverlay();
+ if (!this.overlayVm.shown) {
+ let rect = menuBtn.getBoundingClientRect();
+ this.overlayVm.position = [rect.left + rect.width, rect.bottom];
+ this.overlayVm.shown = true;
+ } else {
+ this.overlayVm.shown = false;
+ }
+ }
+
+ addThreadListBtnIfEnabled(readToggle) {
+ isOptionEnabled('workflows').then(isEnabled => {
+ if (isEnabled) {
+ let tooltip = chrome.i18n.getMessage('inject_workflows_menubtn');
+ let btn = addButtonToThreadListActions(
+ readToggle, 'more_vert', wfDebugId, tooltip);
+ btn.addEventListener('click', () => {
+ this.switchMenu(btn);
+ });
+ }
+ });
+ }
+
+ shouldAddThreadListBtn(node) {
+ return shouldAddBtnToActionBar(wfDebugId, node);
+ }
+};