From 3cb0d27d389bda042239ac2a1cc12c4a7d093cda Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 1 May 2019 18:31:01 -0400 Subject: [PATCH] DEV: Upgrade our widget handlebars compiler Now supports subexpressions such as i18n and concat, plus automatic attaching of widgets similar to ember. --- .../discourse/lib/transform-post.js.es6 | 2 +- .../discourse/widgets/actions-summary.js.es6 | 67 +++---------- .../discourse/widgets/header-contents.js.es6 | 4 +- .../widgets/private-message-map.js.es6 | 6 +- .../discourse/widgets/user-menu.js.es6 | 15 ++- lib/javascripts/widget-hbs-compiler.js.es6 | 97 +++++++++++++------ script/test_hbs_compiler.rb | 20 +--- .../widgets/actions-summary-test.js.es6 | 2 +- test/javascripts/widgets/widget-test.js.es6 | 39 ++++++++ 9 files changed, 142 insertions(+), 110 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index 43aa804ca4..1aa662a853 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -222,7 +222,7 @@ export default function transformPost( acted, count, canUndo: a.can_undo, - canDeferFlags: a.can_defer_flags, + canIgnoreFlags: a.can_defer_flags, description: actionDescription(action, acted, count) }; }); diff --git a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 index 50c6d8a9e2..d15a3d7ba7 100644 --- a/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 +++ b/app/assets/javascripts/discourse/widgets/actions-summary.js.es6 @@ -63,15 +63,12 @@ createWidget("small-user-list", { createWidget("action-link", { tagName: "span.action-link", + template: hbs`{{attrs.text}}. `, buildClasses(attrs) { return attrs.className; }, - html(attrs) { - return h("a", [attrs.text, ". "]); - }, - click() { this.sendWidgetAction(this.attrs.action); } @@ -82,56 +79,24 @@ createWidget("actions-summary-item", { buildKey: attrs => `actions-summary-item-${attrs.id}`, defaultState() { - return { users: [] }; + return { users: null }; }, - html(attrs, state) { - const users = state.users; + template: hbs` + {{#if state.users}} + {{small-user-list users=state.users description=(concat "post.actions.people." attrs.action)}} + {{else}} + {{action-link action="whoActed" text=attrs.description}} + {{/if}} - const result = []; - const action = attrs.action; + {{#if attrs.canUndo}} + {{action-link action="undo" className="undo" text=(i18n (concat "post.actions.undo." attrs.action))}} + {{/if}} - if (users.length === 0) { - result.push( - this.attach("action-link", { - action: "whoActed", - text: attrs.description - }) - ); - } else { - result.push( - this.attach("small-user-list", { - users, - description: `post.actions.people.${action}` - }) - ); - } - - if (attrs.canUndo) { - result.push( - this.attach("action-link", { - action: "undo", - className: "undo", - text: I18n.t(`post.actions.undo.${action}`) - }) - ); - } - - if (attrs.canDeferFlags) { - const flagsDesc = I18n.t(`post.actions.defer_flags`, { - count: attrs.count - }); - result.push( - this.attach("action-link", { - action: "deferFlags", - className: "defer-flags", - text: flagsDesc - }) - ); - } - - return result; - }, + {{#if attrs.canIgnoreFlags}} + {{action-link action="deferFlags" className="defer-flags" text=(i18n "post.actions.defer_flags" count=attrs.count)}} + {{/if}} + `, whoActed() { const attrs = this.attrs; @@ -159,7 +124,7 @@ export default createWidget("actions-summary", { tagName: "section.post-actions", template: hbs` {{#each attrs.actionsSummary as |as|}} - {{attach widget="actions-summary-item" attrs=as}} + {{actions-summary-item attrs=as}}
{{/each}} {{#if attrs.deleted_at}} diff --git a/app/assets/javascripts/discourse/widgets/header-contents.js.es6 b/app/assets/javascripts/discourse/widgets/header-contents.js.es6 index 82bae85ec4..6f4c7089ec 100644 --- a/app/assets/javascripts/discourse/widgets/header-contents.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header-contents.js.es6 @@ -4,9 +4,9 @@ import hbs from "discourse/widgets/hbs-compiler"; createWidget("header-contents", { tagName: "div.contents.clearfix", template: hbs` - {{attach widget="home-logo" attrs=attrs}} + {{home-logo attrs=attrs}} {{#if attrs.topic}} - {{attach widget="header-topic-info" attrs=attrs}} + {{header-topic-info attrs=attrs}} {{/if}}
{{yield}}
` diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 index 64274c4929..faf7ebe164 100644 --- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 @@ -34,9 +34,9 @@ createWidget("pm-map-user-group", { {{attrs.group.name}} {{#if attrs.isEditing}} - {{#if attrs.canRemoveAllowedUsers}} - {{attach widget="pm-remove-group-link" attrs=attrs.group}} - {{/if}} + {{#if attrs.canRemoveAllowedUsers}} + {{pm-remove-group-link attrs=attrs.group}} + {{/if}} {{/if}} ` }); diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index d1a37e4011..efd94baaa0 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -109,15 +109,12 @@ createWidget("user-menu-dismiss-link", { template: hbs` ` diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index a55dfaebf7..fc2280a335 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -5,27 +5,62 @@ function resolve(path) { return path; } +function sexpValue(value) { + if (!value) { + return; + } + + let pValue = value.original; + if (value.type === "StringLiteral") { + return JSON.stringify(pValue); + } else if (value.type === "SubExpression") { + return sexp(value); + } + return pValue; +} + +function pairsToObj(pairs) { + let result = []; + + pairs.forEach(p => { + result.push(`"${p.key}": ${sexpValue(p.value)}`); + }); + + return `{ ${result.join(", ")} }`; +} + +function i18n(node) { + let key = sexpValue(node.params[0]); + + let hash = node.hash; + if (hash.pairs.length) { + return `I18n.t(${key}, ${pairsToObj(hash.pairs)})`; + } + + return `I18n.t(${key})`; +} + function sexp(value) { if (value.path.original === "hash") { + return pairsToObj(value.hash.pairs); + } + + if (value.path.original === "concat") { let result = []; - - value.hash.pairs.forEach(p => { - let pValue = p.value.original; - if (p.value.type === "StringLiteral") { - pValue = JSON.stringify(pValue); - } - - result.push(`"${p.key}": ${pValue}`); + value.params.forEach(p => { + result.push(sexpValue(p)); }); + return result.join(" + "); + } - return `{ ${result.join(", ")} }`; + if (value.path.original === "i18n") { + return i18n(value); } } -function argValue(arg) { - let value = arg.value; +function valueOf(value) { if (value.type === "SubExpression") { - return sexp(arg.value); + return sexp(value); } else if (value.type === "PathExpression") { return value.original; } else if (value.type === "StringLiteral") { @@ -33,6 +68,10 @@ function argValue(arg) { } } +function argValue(arg) { + return valueOf(arg.value); +} + function useHelper(state, name) { let id = state.helpersUsed[name]; if (!id) { @@ -60,17 +99,7 @@ function mustacheValue(node, state) { return `this.attrs.contents()`; break; case "i18n": - let value; - if (node.params[0].type === "StringLiteral") { - value = `"${node.params[0].value}"`; - } else if (node.params[0].type === "PathExpression") { - value = resolve(node.params[0].original); - } - - if (value) { - return `I18n.t(${value})`; - } - + return i18n(node); break; case "avatar": let template = argValue(node.hash.pairs.find(p => p.key === "template")); @@ -82,14 +111,26 @@ function mustacheValue(node, state) { )}(${size}, { template: ${template}, username: ${username} })`; break; case "date": - value = resolve(node.params[0].original); - return `${useHelper(state, "dateNode")}(${value})`; + return `${useHelper(state, "dateNode")}(${valueOf(node.params[0])})`; break; case "d-icon": - let icon = node.params[0].value; - return `${useHelper(state, "iconNode")}("${icon}")`; + return `${useHelper(state, "iconNode")}(${valueOf(node.params[0])})`; break; default: + // Shortcut: If our mustach has hash arguments, we can assume it's attaching. + // For example `{{home-logo count=123}}` can become `this.attach('home-logo, { "count": 123 });` + let hash = node.hash; + if (hash.pairs.length) { + let widgetString = JSON.stringify(path); + // magic: support applying of attrs. This is commonly done like `{{home-logo attrs=attrs}}` + let firstPair = hash.pairs[0]; + if (firstPair.key === "attrs") { + return `this.attach(${widgetString}, ${firstPair.value.original})`; + } + + return `this.attach(${widgetString}, ${pairsToObj(hash.pairs)})`; + } + if (node.escaped) { return `${resolve(path)}`; } else { @@ -168,7 +209,7 @@ class Compiler { case "MustacheStatement": const value = mustacheValue(node, this.state); if (value) { - instructions.push(`${parentAcc}.push(${value})`); + instructions.push(`${parentAcc}.push(${value});`); } break; case "BlockStatement": diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb index e5dec469ad..192e036bf7 100644 --- a/script/test_hbs_compiler.rb +++ b/script/test_hbs_compiler.rb @@ -1,19 +1,9 @@ template = <<~HBS - {{attach widget="widget-name" attrs=attrs}} - {{a}} - {{{htmlValue}}} - {{#if state.category}} - {{attach widget="category-display" attrs=(hash category=state.category someNumber=123 someString="wat")}} - {{/if}} - {{#each transformed.something as |s|}} - {{s.wat}} - {{/each}} - - {{attach widget=settings.widgetName}} - - {{#unless settings.hello}} - XYZ - {{/unless}} + {{attach widget="wat" attrs=(hash test="abc" text=(i18n "hello" count=attrs.wat))}} + {{action-link action="undo" className="undo" text=(i18n (concat "post.actions.undo." attrs.action))}} + {{actions-summary-item attrs=as}} + {{attach widget="actions-summary-item" attrs=as}} + {{testing value="hello"}} HBS ctx = MiniRacer::Context.new(timeout: 15000) diff --git a/test/javascripts/widgets/actions-summary-test.js.es6 b/test/javascripts/widgets/actions-summary-test.js.es6 index b6e09b2c26..9dc926ab46 100644 --- a/test/javascripts/widgets/actions-summary-test.js.es6 +++ b/test/javascripts/widgets/actions-summary-test.js.es6 @@ -53,7 +53,7 @@ widgetTest("deferFlags", { { action: "off_topic", description: "very off topic", - canDeferFlags: true, + canIgnoreFlags: true, count: 1 } ] diff --git a/test/javascripts/widgets/widget-test.js.es6 b/test/javascripts/widgets/widget-test.js.es6 index 4ffcfa2ad3..061e2d5735 100644 --- a/test/javascripts/widgets/widget-test.js.es6 +++ b/test/javascripts/widgets/widget-test.js.es6 @@ -204,6 +204,45 @@ widgetTest("widget attaching", { } }); +widgetTest("magic attaching by name", { + template: `{{mount-widget widget="attach-test"}}`, + + beforeEach() { + createWidget("test-embedded", { tagName: "div.embedded" }); + + createWidget("attach-test", { + tagName: "div.container", + template: hbs`{{test-embedded attrs=attrs}}` + }); + }, + + test(assert) { + assert.ok(find(".container").length, "renders container"); + assert.ok(find(".container .embedded").length, "renders attached"); + } +}); + +widgetTest("custom attrs to a magic attached widget", { + template: `{{mount-widget widget="attach-test"}}`, + + beforeEach() { + createWidget("testing", { + tagName: "span.value", + template: hbs`{{attrs.value}}` + }); + + createWidget("attach-test", { + tagName: "div.container", + template: hbs`{{testing value=(concat "hello" " " "world")}}` + }); + }, + + test(assert) { + assert.ok(find(".container").length, "renders container"); + assert.equal(find(".container .value").text(), "hello world"); + } +}); + widgetTest("handlebars d-icon", { template: `{{mount-widget widget="hbs-icon-test" args=args}}`,