Workflows: support CR variable substitution
Bug: twpowertools:91
Change-Id: Ib973bef40bed42d9c75f15710fd2ac3eeb6b9b15
diff --git a/src/contentScripts/communityConsole/workflows/actionRunners/replyWithCR.js b/src/contentScripts/communityConsole/workflows/actionRunners/replyWithCR.js
index e8f665c..e534ef9 100644
--- a/src/contentScripts/communityConsole/workflows/actionRunners/replyWithCR.js
+++ b/src/contentScripts/communityConsole/workflows/actionRunners/replyWithCR.js
@@ -6,6 +6,8 @@
const kType_RecommendedAnswer = 3;
const kPostMethodCommunityConsole = 4;
+const kVariablesRegex = /\$([A-Za-z_]+)/g;
+
export default class CRRunner {
constructor() {
this._CRs = [];
@@ -36,31 +38,43 @@
});
}
+ _templateSubstitute(payload, thread) {
+ if (!payload.match(kVariablesRegex)) return Promise.resolve(payload);
+
+ return thread.loadThreadDetails().then(() => {
+ return payload.replaceAll(kVariablesRegex, (_, p1) => {
+ return thread?.[p1] ?? '';
+ });
+ });
+ }
+
execute(action, thread) {
let crId = action?.getCannedResponseId?.();
if (!crId)
return Promise.reject(
new Error('The action doesn\'t contain a valid CR id.'));
- return this._getCRPayload(crId).then(payload => {
- let subscribe = action?.getSubscribe?.() ?? false;
- let markAsAnswer = action?.getMarkAsAnswer?.() ?? false;
- return CCApi(
- 'CreateMessage', {
- 1: thread.forum, // forumId
- 2: thread.thread, // threadId
- // message
- 3: {
- 4: payload,
- 6: {
- 1: markAsAnswer ? kType_RecommendedAnswer : kType_Reply,
+ return this._getCRPayload(crId)
+ .then(payload => this._templateSubstitute(payload, thread))
+ .then(payload => {
+ let subscribe = action?.getSubscribe?.() ?? false;
+ let markAsAnswer = action?.getMarkAsAnswer?.() ?? false;
+ return CCApi(
+ 'CreateMessage', {
+ 1: thread.forumId,
+ 2: thread.threadId,
+ // message
+ 3: {
+ 4: payload,
+ 6: {
+ 1: markAsAnswer ? kType_RecommendedAnswer : kType_Reply,
+ },
+ 11: kPostMethodCommunityConsole,
+ },
+ 4: subscribe,
+ 6: kPiiScanType_ScanNone,
},
- 11: kPostMethodCommunityConsole,
- },
- 4: subscribe,
- 6: kPiiScanType_ScanNone,
- },
- /* authenticated = */ true, getAuthUser());
- });
+ /* authenticated = */ true, getAuthUser());
+ });
}
}
diff --git a/src/contentScripts/communityConsole/workflows/models/thread.js b/src/contentScripts/communityConsole/workflows/models/thread.js
new file mode 100644
index 0000000..0f54316
--- /dev/null
+++ b/src/contentScripts/communityConsole/workflows/models/thread.js
@@ -0,0 +1,113 @@
+import {waitFor} from 'poll-until-promise';
+
+import {CCApi} from '../../../../common/api.js';
+import {parseUrl} from '../../../../common/commonUtils.js';
+import {getAuthUser} from '../../../../common/communityConsoleUtils.js';
+
+export default class Thread {
+ constructor(forumId, threadId) {
+ this.forumId = forumId;
+ this.threadId = threadId;
+ this._details = null;
+ }
+
+ static fromUrl(url) {
+ const rawThread = parseUrl(url);
+ if (!rawThread) return null;
+
+ return new Thread(rawThread.forum, rawThread.thread);
+ }
+
+ loadThreadDetails() {
+ if (this._details) return Promise.resolve(true);
+
+ return waitFor(
+ () => {
+ return CCApi(
+ 'ViewForum', {
+ 1: '0', // forumID,
+ // options
+ 2: {
+ 3: false, // withMessages
+ 5: true, // withUserProfile
+ 6: false, // withUserReadState
+ 7: false, // withStickyThreads
+ 9: false, // withRequestorProfile
+ 10: false, // withPromotedMessages
+ 11: false, // withExpertResponder
+ 12: `forum:${this.forumId} thread:${
+ this.threadId}`, // forumViewFilters
+ 16: false, // withThreadNotes
+ 17: false, // withExpertReplyingIndicator
+ },
+ },
+ /* authenticated = */ true, getAuthUser())
+ .then(res => {
+ if (res?.['1']?.['2']?.length < 1)
+ throw new Error(
+ `Couldn't retrieve thread details (forum: ${
+ this.forumId}, thread: ${this.thread}).`);
+
+ return res?.['1']?.['2']?.[0];
+ });
+ },
+ {
+ interval: 500,
+ timeout: 2000,
+ })
+ .then(thread => {
+ this._details = thread;
+ return true;
+ });
+ }
+
+ get opName() {
+ return this._details?.['4']?.['1']?.['1'];
+ }
+
+ get opUserId() {
+ return this._details?.['4']?.['3'];
+ }
+
+ get forumTitle() {
+ return this._details?.['23'];
+ }
+
+ get isRead() {
+ return !!this._details?.['6'];
+ }
+
+ get isStarred() {
+ return !!this._details?.['7']?.['1'];
+ }
+
+ get numMessages() {
+ return this._details?.['8'];
+ }
+
+ get numAnswers() {
+ return this._details?.['15'];
+ }
+
+ get numSuggestedAnswers() {
+ return this._details?.['32'];
+ }
+
+ get title() {
+ return this._details?.['2']?.['9'];
+ }
+
+ get payload() {
+ return this._details?.['2']?.['13'];
+ }
+
+ // Accessors in the style of
+ // https://support.google.com/communities/answer/9147001.
+ get op_name() {
+ return this.opName;
+ }
+
+ get forum_name() {
+ return this.forumTitle;
+ }
+}
diff --git a/src/contentScripts/communityConsole/workflows/runner.js b/src/contentScripts/communityConsole/workflows/runner.js
index 2e90b19..d181d46 100644
--- a/src/contentScripts/communityConsole/workflows/runner.js
+++ b/src/contentScripts/communityConsole/workflows/runner.js
@@ -2,6 +2,7 @@
import * as pb from '../../../workflows/proto/main_pb.js';
import CRRunner from './actionRunners/replyWithCR.js';
+import Thread from './models/thread.js';
export default class WorkflowRunner {
constructor(workflow, updateCallback) {
@@ -32,7 +33,7 @@
const url = recursiveParentElement(checkbox, 'EC-THREAD-SUMMARY')
.querySelector('a.header-content')
.href;
- const thread = parseUrl(url);
+ const thread = Thread.fromUrl(url);
if (!thread) {
console.error('Couldn\'t parse URL ' + url);
continue;
diff --git a/src/workflows/manager/components/actions/ReplyWithCR.js b/src/workflows/manager/components/actions/ReplyWithCR.js
index f1bdeeb..e3deee7 100644
--- a/src/workflows/manager/components/actions/ReplyWithCR.js
+++ b/src/workflows/manager/components/actions/ReplyWithCR.js
@@ -2,7 +2,7 @@
import '@material/web/switch/switch.js';
import '@material/web/textfield/outlined-text-field.js';
-import {css, html, LitElement} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
import {createRef, ref} from 'lit/directives/ref.js';
import {CCApi} from '../../../../common/api.js';
@@ -54,11 +54,13 @@
?readonly=${this.readOnly}
@input=${this._cannedResponseIdChanged}>
</md-outlined-text-field>
- <md-outlined-button
- icon="more"
- label="Select CR"
- @click=${this._openCRImporter}>
- </md-outlined-button>
+ ${this.readOnly ? nothing : html`
+ <md-outlined-button
+ icon="more"
+ label="Select CR"
+ @click=${this._openCRImporter}>
+ </md-outlined-button>
+ `}
</div>
<div class="form-line">
<md-formfield label="Subscribe to thread">