jenkins-bot has submitted this change and it was merged. Change subject: EventLog Flow topic and post actions (FlowReplies) ......................................................................
EventLog Flow topic and post actions (FlowReplies) This will track all events defined in https://meta.wikimedia.org/wiki/Schema:FlowReplies It works by setting some data-attributes on nodes that you want to track (on click). Those attributes are: * data-flow-eventlog-schema: name of the schema * data-flow-eventlog-entrypoint: name of the entrypoint param * data-flow-eventlog-action: name of the action param * data-flow-eventlog-forward: selectors to nodes to forward to This forwarding is because we track nodes in a funnel. Most properties for subsequent calls in a funnel are the same - only action will be different. For follow-up nodes to be tracked in a funnel, the only data-attribute that is needed is data-flow-eventlog-action (which is what the clickhandler is bound to) Since things in Flow happen through a variety of ways, we can't use the click handler for everything we want to log: * opening a new-topic or reply-bottom form is via a focusin event * cancel-attempt, cancel-abort & cancel-success are done by JS * preview & keep-editing are done by JS I've added a way to add interactiveHandlers on focus (patch prior to this) so we can deal with the former. The latter is solved by directly calling the log code from JS. This way, this eventlogging stuff is at least still *mostly* driven by data attributes. I've also made all interactiveHandlers & apiHandlers (patch prior to this) return deferred objects, which only resolve (or reject) when all of the work is really done. That way, we can delay the forwarding until we're certain the nodes will have already been rendered (that's async) Change-Id: Ic011a6130184aadae539eced307bf982f84c990d --- M Flow.php M Resources.php M handlebars/compiled/flow_block_topic.handlebars.php M handlebars/compiled/flow_block_topic_moderate_post.handlebars.php M handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php M handlebars/compiled/flow_block_topiclist.handlebars.php M handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php M handlebars/compiled/flow_block_topicsummary_edit.handlebars.php M handlebars/compiled/flow_form_buttons.handlebars.php M handlebars/compiled/flow_post.handlebars.php M handlebars/flow_form_buttons.handlebars M handlebars/flow_newtopic_form.handlebars M handlebars/flow_post_meta_actions.handlebars M handlebars/flow_reply_form.handlebars M handlebars/flow_topic_titlebar_content.handlebars M modules/engine/components/board/base/flow-board-api-events.js M modules/engine/components/board/base/flow-board-interactive-events.js M modules/engine/components/board/base/flow-board-misc.js M modules/engine/components/board/base/flow-boardandhistory-base.js M modules/engine/components/common/flow-component-engines.js M modules/engine/components/common/flow-component-events.js A modules/engine/misc/flow-eventlog.js 22 files changed, 598 insertions(+), 59 deletions(-) Approvals: EBernhardson: Looks good to me, approved jenkins-bot: Verified diff --git a/Flow.php b/Flow.php index f03258d..8c68f64 100644 --- a/Flow.php +++ b/Flow.php @@ -287,3 +287,6 @@ // of sending Cookie headers to Parsoid over HTTP. For security reasons, it is strongly recommended // that $wgVisualEditorParsoidURL be pointed to localhost if this setting is enabled. $wgFlowParsoidForwardCookies = false; + +// Enable/disable event logging +$wgFlowEventLogging = true; diff --git a/Resources.php b/Resources.php index 8a73256..7cbd613 100644 --- a/Resources.php +++ b/Resources.php @@ -319,6 +319,8 @@ 'engine/misc/mw-ui.modal.js', // FlowApi 'engine/misc/flow-api.js', + // FlowEventLog + 'engine/misc/flow-eventlog.js', // Component registry 'engine/components/flow-registry.js', // FlowComponent must come before actual components @@ -402,3 +404,22 @@ ), ) + $mobile, ); + +$wgHooks['ResourceLoaderRegisterModules'][] = function ( ResourceLoader &$resourceLoader ) { + global $wgFlowEventLogging, $wgResourceModules; + + // Only if EventLogging in Flow is enabled & EventLogging exists + if ( $wgFlowEventLogging && class_exists( 'ResourceLoaderSchemaModule' ) ) { + $resourceLoader->register( 'schema.FlowReplies', array( + 'class' => 'ResourceLoaderSchemaModule', + 'schema' => 'FlowReplies', + // See https://meta.wikimedia.org/wiki/Schema:FlowReplies, below title + 'revision' => 10561344, + ) ); + + // Add as dependency to Flow JS + $wgResourceModules['ext.flow']['dependencies'][] = 'schema.FlowReplies'; + } + + return true; +}; diff --git a/handlebars/compiled/flow_block_topic.handlebars.php b/handlebars/compiled/flow_block_topic.handlebars.php index 9c7540d..f691efa 100644 --- a/handlebars/compiled/flow_block_topic.handlebars.php +++ b/handlebars/compiled/flow_block_topic.handlebars.php @@ -57,6 +57,16 @@ title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet" data-flow-interactive-handler="activateForm" + + + data-flow-eventlog-schema="FlowReplies" + data-flow-eventlog-action="initiate" + data-flow-eventlog-entrypoint="reply-top" + data-flow-eventlog-forward=" + < .flow-topic .flow-reply-form:last [data-role=\'cancel\'], + < .flow-topic .flow-reply-form:last [data-role=\'action\'][name=\'preview\'], + < .flow-topic .flow-reply-form:last [data-role=\'submit\'] + " >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> • ' : '').' @@ -88,7 +98,6 @@ class="mw-ui-anchor mw-ui-constructive '.((!LCRun3::ifvar($cx, ((isset($in['isWatched']) && is_array($in)) ? $in['isWatched'] : null))) ? 'mw-ui-quiet' : '').' '.((LCRun3::ifvar($cx, ((isset($in['isWatched']) && is_array($in)) ? $in['isWatched'] : null))) ? 'flow-watch-link-unwatch' : 'flow-watch-link-watch').'" data-flow-api-handler="watchItem" - data-flow-api-handler="watchTopic" data-flow-api-target="< .flow-topic-watchlist" data-flow-api-method="POST">'.htmlentities((string)((isset($in['null']) && is_array($in)) ? $in['null'] : null), ENT_QUOTES, 'UTF-8').'<span class="wikiglyph wikiglyph-star"></span>'.htmlentities((string)((isset($in['null']) && is_array($in)) ? $in['null'] : null), ENT_QUOTES, 'UTF-8').''.htmlentities((string)((isset($in['null']) && is_array($in)) ? $in['null'] : null), ENT_QUOTES, 'UTF-8').'<span class="wikiglyph wikiglyph-unstar"></span>'.htmlentities((string)((isset($in['null']) && is_array($in)) ? $in['null'] : null), ENT_QUOTES, 'UTF-8').'</a> </div> @@ -203,12 +212,17 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> ';},'flow_reply_form' => function ($cx, $in) {return ''.((LCRun3::ifvar($cx, ((isset($in['actions']['reply']) && is_array($in['actions'])) ? $in['actions']['reply'] : null))) ? ' <form class="flow-post flow-reply-form" @@ -230,17 +244,23 @@ required data-flow-preview-template="flow_post" data-flow-expandable="true" - class="mw-ui-input" + class="mw-ui-input flow-click-interactive" type="text" placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post']) && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : null)),Array()), 'encq').'" - data-role="content">'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> + data-role="content" + + + data-flow-interactive-handler-focus="activateReplyTopic" + >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> <div class="flow-form-actions flow-form-collapsible"> <button data-role="submit" class="mw-ui-button mw-ui-constructive" data-flow-interactive-handler="apiRequest" data-flow-api-handler="submitReply" - data-flow-api-target="< .flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> + data-flow-api-target="< .flow-topic" + data-flow-eventlog-action="save-attempt" + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> '.LCRun3::p($cx, 'flow_form_buttons', Array(Array($in),Array())).' <small class="flow-terms-of-use plainlinks">'.LCRun3::ch($cx, 'l10nParse', Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small> </div> @@ -304,4 +324,4 @@ </div> '; } -?> +?> \ No newline at end of file diff --git a/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php b/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php index 36bb397..7748d0b 100644 --- a/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php +++ b/handlebars/compiled/flow_block_topic_moderate_post.handlebars.php @@ -84,7 +84,18 @@ <a href="'.htmlentities((string)((isset($in['actions']['reply']['url']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['url'] : null), ENT_QUOTES, 'UTF-8').'" title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet" - data-flow-interactive-handler="activateReplyPost">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> + data-flow-interactive-handler="activateReplyPost" + + + data-flow-eventlog-schema="FlowReplies" + data-flow-eventlog-action="initiate" + data-flow-eventlog-entrypoint="reply-post" + data-flow-eventlog-forward=" + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'cancel\'], + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'action\'][name=\'preview\'], + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'submit\'] + " + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> ' : '').' '.((LCRun3::ifvar($cx, ((isset($in['actions']['edit']) && is_array($in['actions'])) ? $in['actions']['edit'] : null))) ? ' <a href="'.htmlentities((string)((isset($in['actions']['edit']['url']) && is_array($in['actions']['edit'])) ? $in['actions']['edit']['url'] : null), ENT_QUOTES, 'UTF-8').'" @@ -253,12 +264,16 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + data-flow-eventlog-action="preview" >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> ';},'flow_edit_post' => function ($cx, $in) {return '<form class="flow-edit-post-form" method="POST" @@ -302,17 +317,23 @@ required data-flow-preview-template="flow_post" data-flow-expandable="true" - class="mw-ui-input" + class="mw-ui-input flow-click-interactive" type="text" placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post']) && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : null)),Array()), 'encq').'" - data-role="content">'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> + data-role="content" + + + data-flow-interactive-handler-focus="activateReplyTopic" + >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> <div class="flow-form-actions flow-form-collapsible"> <button data-role="submit" class="mw-ui-button mw-ui-constructive" data-flow-interactive-handler="apiRequest" data-flow-api-handler="submitReply" - data-flow-api-target="< .flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> + data-flow-api-target="< .flow-topic" + data-flow-eventlog-action="save-attempt" + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> '.LCRun3::p($cx, 'flow_form_buttons', Array(Array($in),Array())).' <small class="flow-terms-of-use plainlinks">'.LCRun3::ch($cx, 'l10nParse', Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small> </div> diff --git a/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php b/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php index fa646a7..3a3d05a 100644 --- a/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php +++ b/handlebars/compiled/flow_block_topic_moderate_topic.handlebars.php @@ -84,7 +84,18 @@ <a href="'.htmlentities((string)((isset($in['actions']['reply']['url']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['url'] : null), ENT_QUOTES, 'UTF-8').'" title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet" - data-flow-interactive-handler="activateReplyPost">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> + data-flow-interactive-handler="activateReplyPost" + + + data-flow-eventlog-schema="FlowReplies" + data-flow-eventlog-action="initiate" + data-flow-eventlog-entrypoint="reply-post" + data-flow-eventlog-forward=" + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'cancel\'], + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'action\'][name=\'preview\'], + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'submit\'] + " + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> ' : '').' '.((LCRun3::ifvar($cx, ((isset($in['actions']['edit']) && is_array($in['actions'])) ? $in['actions']['edit'] : null))) ? ' <a href="'.htmlentities((string)((isset($in['actions']['edit']['url']) && is_array($in['actions']['edit'])) ? $in['actions']['edit']['url'] : null), ENT_QUOTES, 'UTF-8').'" @@ -253,12 +264,16 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + data-flow-eventlog-action="preview" >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> ';},'flow_edit_post' => function ($cx, $in) {return '<form class="flow-edit-post-form" method="POST" @@ -302,17 +317,23 @@ required data-flow-preview-template="flow_post" data-flow-expandable="true" - class="mw-ui-input" + class="mw-ui-input flow-click-interactive" type="text" placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post']) && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : null)),Array()), 'encq').'" - data-role="content">'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> + data-role="content" + + + data-flow-interactive-handler-focus="activateReplyTopic" + >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> <div class="flow-form-actions flow-form-collapsible"> <button data-role="submit" class="mw-ui-button mw-ui-constructive" data-flow-interactive-handler="apiRequest" data-flow-api-handler="submitReply" - data-flow-api-target="< .flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> + data-flow-api-target="< .flow-topic" + data-flow-eventlog-action="save-attempt" + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> '.LCRun3::p($cx, 'flow_form_buttons', Array(Array($in),Array())).' <small class="flow-terms-of-use plainlinks">'.LCRun3::ch($cx, 'l10nParse', Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small> </div> diff --git a/handlebars/compiled/flow_block_topiclist.handlebars.php b/handlebars/compiled/flow_block_topiclist.handlebars.php index 2fc5637..9e87b2e 100644 --- a/handlebars/compiled/flow_block_topiclist.handlebars.php +++ b/handlebars/compiled/flow_block_topiclist.handlebars.php @@ -106,12 +106,17 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> ';},'flow_newtopic_form' => function ($cx, $in) {return ''.((LCRun3::ifvar($cx, ((isset($in['actions']['newtopic']) && is_array($in['actions'])) ? $in['actions']['newtopic'] : null))) ? ' <form action="'.htmlentities((string)((isset($in['actions']['newtopic']['url']) && is_array($in['actions']['newtopic'])) ? $in['actions']['newtopic']['url'] : null), ENT_QUOTES, 'UTF-8').'" method="POST" class="flow-newtopic-form" data-flow-initial-state="collapsed"> @@ -125,17 +130,27 @@ <input type="hidden" name="topiclist_replyTo" value="'.htmlentities((string)((isset($in['workflowId']) && is_array($in)) ? $in['workflowId'] : null), ENT_QUOTES, 'UTF-8').'" /> <input name="topiclist_topic" class="mw-ui-input mw-ui-input-large" required - type="text" placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'" data-role="title"/> + type="text" + placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'" + data-role="title" + + + data-flow-interactive-handler-focus="activateNewTopic" + /> <textarea name="topiclist_content" data-flow-preview-template="flow_topic" class="mw-ui-input flow-form-collapsible mw-ui-input-large" '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : '').' - placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 'encq').'" data-role="content" required></textarea> + placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 'encq').'" + data-role="content" + required + ></textarea> <div class="flow-form-actions flow-form-collapsible" '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : '').'> <button data-role="submit" data-flow-api-handler="newTopic" data-flow-interactive-handler="apiRequest" + data-flow-eventlog-action="save-attempt" class="mw-ui-button mw-ui-constructive mw-ui-flush-right">'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-save'),Array()), 'encq').'</button> '.LCRun3::p($cx, 'flow_form_buttons', Array(Array($in),Array())).' <small class="flow-terms-of-use plainlinks">'.LCRun3::ch($cx, 'l10nParse', Array(Array('flow-terms-of-use-new-topic'),Array()), 'encq').'</small> @@ -161,6 +176,16 @@ title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet" data-flow-interactive-handler="activateForm" + + + data-flow-eventlog-schema="FlowReplies" + data-flow-eventlog-action="initiate" + data-flow-eventlog-entrypoint="reply-top" + data-flow-eventlog-forward=" + < .flow-topic .flow-reply-form:last [data-role=\'cancel\'], + < .flow-topic .flow-reply-form:last [data-role=\'action\'][name=\'preview\'], + < .flow-topic .flow-reply-form:last [data-role=\'submit\'] + " >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> • ' : '').' @@ -307,17 +332,23 @@ required data-flow-preview-template="flow_post" data-flow-expandable="true" - class="mw-ui-input" + class="mw-ui-input flow-click-interactive" type="text" placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post']) && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : null)),Array()), 'encq').'" - data-role="content">'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> + data-role="content" + + + data-flow-interactive-handler-focus="activateReplyTopic" + >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> <div class="flow-form-actions flow-form-collapsible"> <button data-role="submit" class="mw-ui-button mw-ui-constructive" data-flow-interactive-handler="apiRequest" data-flow-api-handler="submitReply" - data-flow-api-target="< .flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> + data-flow-api-target="< .flow-topic" + data-flow-eventlog-action="save-attempt" + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> '.LCRun3::p($cx, 'flow_form_buttons', Array(Array($in),Array())).' <small class="flow-terms-of-use plainlinks">'.LCRun3::ch($cx, 'l10nParse', Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small> </div> @@ -412,4 +443,4 @@ </div> '; } -?> +?> \ No newline at end of file diff --git a/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php b/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php index 8af2a89..09383e7 100644 --- a/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php +++ b/handlebars/compiled/flow_block_topiclist_newtopic.handlebars.php @@ -50,12 +50,17 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> ';},'flow_newtopic_form' => function ($cx, $in) {return ''.((LCRun3::ifvar($cx, ((isset($in['actions']['newtopic']) && is_array($in['actions'])) ? $in['actions']['newtopic'] : null))) ? ' <form action="'.htmlentities((string)((isset($in['actions']['newtopic']['url']) && is_array($in['actions']['newtopic'])) ? $in['actions']['newtopic']['url'] : null), ENT_QUOTES, 'UTF-8').'" method="POST" class="flow-newtopic-form" data-flow-initial-state="collapsed"> @@ -69,17 +74,27 @@ <input type="hidden" name="topiclist_replyTo" value="'.htmlentities((string)((isset($in['workflowId']) && is_array($in)) ? $in['workflowId'] : null), ENT_QUOTES, 'UTF-8').'" /> <input name="topiclist_topic" class="mw-ui-input mw-ui-input-large" required - type="text" placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'" data-role="title"/> + type="text" + placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-start-placeholder'),Array()), 'encq').'" + data-role="title" + + + data-flow-interactive-handler-focus="activateNewTopic" + /> <textarea name="topiclist_content" data-flow-preview-template="flow_topic" class="mw-ui-input flow-form-collapsible mw-ui-input-large" '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : '').' - placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 'encq').'" data-role="content" required></textarea> + placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-content-placeholder',((isset($cx['scopes'][0]['title']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['title'] : null)),Array()), 'encq').'" + data-role="content" + required + ></textarea> <div class="flow-form-actions flow-form-collapsible" '.((LCRun3::ifvar($cx, ((isset($in['isOnFlowBoard']) && is_array($in)) ? $in['isOnFlowBoard'] : null))) ? 'style="display:none;"' : '').'> <button data-role="submit" data-flow-api-handler="newTopic" data-flow-interactive-handler="apiRequest" + data-flow-eventlog-action="save-attempt" class="mw-ui-button mw-ui-constructive mw-ui-flush-right">'.LCRun3::ch($cx, 'l10n', Array(Array('flow-newtopic-save'),Array()), 'encq').'</button> '.LCRun3::p($cx, 'flow_form_buttons', Array(Array($in),Array())).' <small class="flow-terms-of-use plainlinks">'.LCRun3::ch($cx, 'l10nParse', Array(Array('flow-terms-of-use-new-topic'),Array()), 'encq').'</small> diff --git a/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php b/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php index 3470667..efe8256 100644 --- a/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php +++ b/handlebars/compiled/flow_block_topicsummary_edit.handlebars.php @@ -32,12 +32,17 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> ';},), 'scopes' => Array($in), diff --git a/handlebars/compiled/flow_form_buttons.handlebars.php b/handlebars/compiled/flow_form_buttons.handlebars.php index 2a28d1f..bf781b7 100644 --- a/handlebars/compiled/flow_form_buttons.handlebars.php +++ b/handlebars/compiled/flow_form_buttons.handlebars.php @@ -24,12 +24,17 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> '; } diff --git a/handlebars/compiled/flow_post.handlebars.php b/handlebars/compiled/flow_post.handlebars.php index ff50df3..9082cff 100644 --- a/handlebars/compiled/flow_post.handlebars.php +++ b/handlebars/compiled/flow_post.handlebars.php @@ -62,7 +62,18 @@ <a href="'.htmlentities((string)((isset($in['actions']['reply']['url']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['url'] : null), ENT_QUOTES, 'UTF-8').'" title="'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet" - data-flow-interactive-handler="activateReplyPost">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> + data-flow-interactive-handler="activateReplyPost" + + + data-flow-eventlog-schema="FlowReplies" + data-flow-eventlog-action="initiate" + data-flow-eventlog-entrypoint="reply-post" + data-flow-eventlog-forward=" + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'cancel\'], + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'action\'][name=\'preview\'], + < .flow-post:not([data-flow-post-max-depth=\'1\']) .flow-reply-form [data-role=\'submit\'] + " + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</a> ' : '').' '.((LCRun3::ifvar($cx, ((isset($in['actions']['edit']) && is_array($in['actions'])) ? $in['actions']['edit'] : null))) ? ' <a href="'.htmlentities((string)((isset($in['actions']['edit']['url']) && is_array($in['actions']['edit'])) ? $in['actions']['edit']['url'] : null), ENT_QUOTES, 'UTF-8').'" @@ -231,12 +242,16 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + data-flow-eventlog-action="preview" >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-preview'),Array()), >'encq').'</button> <button data-flow-interactive-handler="cancelForm" data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + + >'.LCRun3::ch($cx, 'l10n', Array(Array('flow-cancel'),Array()), >'encq').'</button> ';},'flow_edit_post' => function ($cx, $in) {return '<form class="flow-edit-post-form" method="POST" @@ -280,17 +295,23 @@ required data-flow-preview-template="flow_post" data-flow-expandable="true" - class="mw-ui-input" + class="mw-ui-input flow-click-interactive" type="text" placeholder="'.LCRun3::ch($cx, 'l10n', Array(Array('flow-reply-topic-title-placeholder',((isset($in['properties']['topic-of-post']) && is_array($in['properties'])) ? $in['properties']['topic-of-post'] : null)),Array()), 'encq').'" - data-role="content">'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> + data-role="content" + + + data-flow-interactive-handler-focus="activateReplyTopic" + >'.((LCRun3::ifvar($cx, ((isset($cx['scopes'][0]['submitted']) && is_array($cx['scopes'][0])) ? $cx['scopes'][0]['submitted'] : null))) ? ''.LCRun3::hbch($cx, 'ifCond', Array(Array(((isset($cx['scopes'][0]['submitted']['postId']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['postId'] : null),'===',((isset($in['postId']) && is_array($in)) ? $in['postId'] : null)),Array()), $in, function($cx, $in) {return ''.htmlentities((string)((isset($cx['scopes'][0]['submitted']['content']) && is_array($cx['scopes'][0]['submitted'])) ? $cx['scopes'][0]['submitted']['content'] : null), ENT_QUOTES, 'UTF-8').'';}).'' : '').'</textarea> <div class="flow-form-actions flow-form-collapsible"> <button data-role="submit" class="mw-ui-button mw-ui-constructive" data-flow-interactive-handler="apiRequest" data-flow-api-handler="submitReply" - data-flow-api-target="< .flow-topic">'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> + data-flow-api-target="< .flow-topic" + data-flow-eventlog-action="save-attempt" + >'.htmlentities((string)((isset($in['actions']['reply']['title']) && is_array($in['actions']['reply'])) ? $in['actions']['reply']['title'] : null), ENT_QUOTES, 'UTF-8').'</button> '.LCRun3::p($cx, 'flow_form_buttons', Array(Array($in),Array())).' <small class="flow-terms-of-use plainlinks">'.LCRun3::ch($cx, 'l10nParse', Array(Array('flow-terms-of-use-reply'),Array()), 'encq').'</small> </div> diff --git a/handlebars/flow_form_buttons.handlebars b/handlebars/flow_form_buttons.handlebars index 52d155c..64bda0d 100644 --- a/handlebars/flow_form_buttons.handlebars +++ b/handlebars/flow_form_buttons.handlebars @@ -3,6 +3,8 @@ name="preview" data-role="action" class="mw-ui-button mw-ui-progressive mw-ui-quiet mw-ui-flush-right flow-js" + + {{!-- No data-flow-eventlog-action here; we'll do that in code because the action will toggle between preview & keep-editing --}} > {{~l10n "flow-preview"~}} </button> @@ -11,6 +13,9 @@ data-role="cancel" type="reset" class="mw-ui-button mw-ui-destructive mw-ui-quiet mw-ui-flush-right flow-js" + + {{!-- No data-flow-eventlog-action here; we'll do that in code to make sure it's run before cancel-success & cancel-abort --}} + {{!-- funnel id will have been forwarded to this button though, so we can access that from the code --}} > {{~l10n "flow-cancel"~}} </button> diff --git a/handlebars/flow_newtopic_form.handlebars b/handlebars/flow_newtopic_form.handlebars index a798e70..e43c731 100644 --- a/handlebars/flow_newtopic_form.handlebars +++ b/handlebars/flow_newtopic_form.handlebars @@ -10,17 +10,32 @@ <input type="hidden" name="topiclist_replyTo" value="{{ workflowId }}" /> <input name="topiclist_topic" class="mw-ui-input mw-ui-input-large" required - type="text" placeholder="{{l10n "flow-newtopic-start-placeholder"}}" data-role="title"/> + type="text" + placeholder="{{l10n "flow-newtopic-start-placeholder"}}" + data-role="title" + + {{!-- + You'd expect data-flow-eventlog-* data here (this one + needs to be clicked to expand the form). That stuff will be + in JS though, since we only want it on initial focus (activating + the form) + --}} + data-flow-interactive-handler-focus="activateNewTopic" + /> <textarea name="topiclist_content" data-flow-preview-template="flow_topic" class="mw-ui-input flow-form-collapsible mw-ui-input-large" {{#if isOnFlowBoard}}style="display:none;"{{/if}} - placeholder="{{l10n "flow-newtopic-content-placeholder" @root.title}}" data-role="content" required></textarea> + placeholder="{{l10n "flow-newtopic-content-placeholder" @root.title}}" + data-role="content" + required + ></textarea> <div class="flow-form-actions flow-form-collapsible" {{#if isOnFlowBoard}}style="display:none;"{{/if}}> <button data-role="submit" data-flow-api-handler="newTopic" data-flow-interactive-handler="apiRequest" + data-flow-eventlog-action="save-attempt" class="mw-ui-button mw-ui-constructive mw-ui-flush-right">{{l10n "flow-newtopic-save"}}</button> {{> flow_form_buttons }} <small class="flow-terms-of-use plainlinks">{{l10nParse "flow-terms-of-use-new-topic"}}</small> diff --git a/handlebars/flow_post_meta_actions.handlebars b/handlebars/flow_post_meta_actions.handlebars index ca0d74b..1ed10d3 100644 --- a/handlebars/flow_post_meta_actions.handlebars +++ b/handlebars/flow_post_meta_actions.handlebars @@ -4,7 +4,21 @@ <a href="{{actions.reply.url}}" title="{{actions.reply.title}}" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet" - data-flow-interactive-handler="activateReplyPost"> + data-flow-interactive-handler="activateReplyPost" + + {{!-- + Initialize EventLogging: see flow_topic_titlebar_content for + more details on how this works. + --}} + data-flow-eventlog-schema="FlowReplies" + data-flow-eventlog-action="initiate" + data-flow-eventlog-entrypoint="reply-post" + data-flow-eventlog-forward=" + < .flow-post:not([data-flow-post-max-depth='1']) .flow-reply-form [data-role='cancel'], + < .flow-post:not([data-flow-post-max-depth='1']) .flow-reply-form [data-role='action'][name='preview'], + < .flow-post:not([data-flow-post-max-depth='1']) .flow-reply-form [data-role='submit'] + " + > {{~actions.reply.title~}} </a> {{/if}} diff --git a/handlebars/flow_reply_form.handlebars b/handlebars/flow_reply_form.handlebars index 7429135..ce58032 100644 --- a/handlebars/flow_reply_form.handlebars +++ b/handlebars/flow_reply_form.handlebars @@ -18,10 +18,24 @@ required data-flow-preview-template="flow_post" data-flow-expandable="true" - class="mw-ui-input" + class="mw-ui-input flow-click-interactive" type="text" placeholder="{{l10n "flow-reply-topic-title-placeholder" properties.topic-of-post}}" - data-role="content"> + data-role="content" + + {{!-- + You'd expect data-flow-eventlog-* data here (this one + needs to be clicked to expand the form). + However, this form is used in multiple places: as topic- + level reply form (activated by clicking the textarea to + expand), or to reply to a post (activated by clicking the + "reply" link). + We only want to track the former, so we'll do that in JS so + we can ignore all focuses for this textarea when it's not + used to activate the topic-level reply form. + --}} + data-flow-interactive-handler-focus="activateReplyTopic" + > {{~#if @root.submitted~}} {{~#ifCond @root.submitted.postId "===" postId~}} {{[email protected]~}} @@ -34,7 +48,9 @@ class="mw-ui-button mw-ui-constructive" data-flow-interactive-handler="apiRequest" data-flow-api-handler="submitReply" - data-flow-api-target="< .flow-topic"> + data-flow-api-target="< .flow-topic" + data-flow-eventlog-action="save-attempt" + > {{~actions.reply.title~}} </button> {{> flow_form_buttons }} diff --git a/handlebars/flow_topic_titlebar_content.handlebars b/handlebars/flow_topic_titlebar_content.handlebars index 5036df7..f4a3f3d 100644 --- a/handlebars/flow_topic_titlebar_content.handlebars +++ b/handlebars/flow_topic_titlebar_content.handlebars @@ -5,6 +5,29 @@ title="{{actions.reply.title}}" class="mw-ui-anchor mw-ui-progressive mw-ui-quiet" data-flow-interactive-handler="activateForm" + + {{!-- + Initialize EventLogging: + * action: name of the action param + * schema: name of the schema (will be forwarded) + * entrypoint: name of the entrypoint (will be forwarded) + * forward: nodes to forward this funnel to + We want to keep track of multiple actions in the same "funnel". + Having a node without data-flow-eventlog-funnel-id (this node) + will result in a funnel being created. That funnel id will then + be forwarded to all specified nodes, so if you later click on one + of the forwarded nodes, it'll recognize and find the funnel. All + that is needed there, is a specific data-flow-eventlog-action, + all other details (log, entrypoint, funnel id, ...) are inherited + --}} + data-flow-eventlog-schema="FlowReplies" + data-flow-eventlog-action="initiate" + data-flow-eventlog-entrypoint="reply-top" + data-flow-eventlog-forward=" + < .flow-topic .flow-reply-form:last [data-role='cancel'], + < .flow-topic .flow-reply-form:last [data-role='action'][name='preview'], + < .flow-topic .flow-reply-form:last [data-role='submit'] + " > {{~actions.reply.title~}} </a> diff --git a/modules/engine/components/board/base/flow-board-api-events.js b/modules/engine/components/board/base/flow-board-api-events.js index ff34899..dc6f305 100644 --- a/modules/engine/components/board/base/flow-board-api-events.js +++ b/modules/engine/components/board/base/flow-board-api-events.js @@ -164,7 +164,12 @@ FlowBoardComponentApiEventsMixin.UI.events.apiPreHandlers.preview = function ( event ) { var callback, $this = $( this ), - flowBoard = mw.flow.getPrototypeMethod( 'board', 'getInstanceByElement' )( $this ); + flowBoard = mw.flow.getPrototypeMethod( 'board', 'getInstanceByElement' )( $this ), + schemaName = $( this ).data( 'flow-eventlog-schema' ), + funnelId = $( this ).data( 'flow-eventlog-funnel-id' ), + logAction = $( this ).data( 'flow-return-to-edit' ) ? 'keep-editing' : 'preview'; + + flowBoard.logEvent( schemaName, { action: logAction, funnelId: funnelId } ); callback = function ( queryMap ) { var content = null; @@ -691,13 +696,14 @@ // Hide cancel button on preview screen $cancelButton.hide(); + // Assign the reset-preview information for later use $button .data( 'flow-return-to-edit', { text: $button.text(), - $nodes: $previewContainer + $nodes: $previewContainer, } ) - .text( flowBoard.constructor.static.TemplateEngine.l10n('flow-preview-return-edit-post') ) + .text( flowBoard.constructor.static.TemplateEngine.l10n( 'flow-preview-return-edit-post' ) ) .one( 'click', function() { $cancelButton.show(); } ); @@ -714,12 +720,16 @@ */ FlowBoardComponentApiEventsMixin.UI.events.apiHandlers.newTopic = function ( info, data, jqxhr ) { var result, html, + schemaName = $( this ).data( 'flow-eventlog-schema' ), + funnelId = $( this ).data( 'flow-eventlog-funnel-id' ), flowBoard = mw.flow.getPrototypeMethod( 'board', 'getInstanceByElement' )( $( this ) ); if ( info.status !== 'done' ) { // Error will be displayed by default, nothing else to wrap up return $.Deferred().reject().promise(); } + + flowBoard.logEvent( schemaName, { action: 'save-success', funnelId: funnelId } ); result = data.flow['new-topic'].result.topiclist; @@ -751,13 +761,18 @@ */ FlowBoardComponentApiEventsMixin.UI.events.apiHandlers.submitReply = function ( info, data, jqxhr ) { var $form = $( this ).closest( 'form' ), - flowBoard = mw.flow.getPrototypeMethod( 'board', 'getInstanceByElement' )( $form ); + flowBoard = mw.flow.getPrototypeMethod( 'board', 'getInstanceByElement' )( $form ), + schemaName = $( this ).data( 'flow-eventlog-schema' ), + funnelId = $( this ).data( 'flow-eventlog-funnel-id' ); if ( info.status !== 'done' ) { // Error will be displayed by default, nothing else to wrap up return $.Deferred().reject().promise(); } + flowBoard.logEvent( schemaName, { action: 'save-success', funnelId: funnelId } ); + + // Execute cancel callback to destroy form flowBoard.emitWithReturn( 'cancelForm', $form ); // Target should be flow-topic diff --git a/modules/engine/components/board/base/flow-board-interactive-events.js b/modules/engine/components/board/base/flow-board-interactive-events.js index 6663b01..0659808 100644 --- a/modules/engine/components/board/base/flow-board-interactive-events.js +++ b/modules/engine/components/board/base/flow-board-interactive-events.js @@ -168,6 +168,82 @@ * @param {Event} event * @returns {$.Promise} */ + FlowBoardComponentInteractiveEventsMixin.UI.events.interactiveHandlers.activateReplyTopic = function ( event ) { + var $topic = $( this ).closest( '.flow-topic' ), + topicId = $topic.data( 'flow-id' ), + component; + + // The reply form is used in multiple places. This will check if it was + // triggered from inside the topic reply form. + if ( $( this ).closest( '#flow-reply-' + topicId ).length === 0 ) { + // Not in topic reply form + return $.Deferred().reject(); + } + + // Only if the textarea is compressed, is it being activated. Otherwise, + // it has already expanded and this focus is now just re-focussing the + // already active form + if ( !$( this ).hasClass( 'flow-input-compressed' ) ) { + // Form already activated + return $.Deferred().reject(); + } + + component = mw.flow.getPrototypeMethod( 'component', 'getInstanceByElement' )( $( this ) ); + component.logEvent( + 'FlowReplies', + // log data + { + entrypoint: 'reply-bottom', + action: 'initiate' + }, + // nodes to forward funnel to + $( this ).findWithParent( + '< .flow-reply-form [data-role="cancel"],' + + '< .flow-reply-form [data-role="action"][name="preview"],' + + '< .flow-reply-form [data-role="submit"]' + ) + ); + + return $.Deferred().resolve(); + }; + + /** + * @param {Event} event + */ + FlowBoardComponentInteractiveEventsMixin.UI.events.interactiveHandlers.activateNewTopic = function ( event ) { + var $form = $( this ).closest( '.flow-newtopic-form' ), + component; + + // Only if the textarea is compressed, is it being activated. Otherwise, + // it has already expanded and this focus is now just re-focussing the + // already active form + if ( $form.find( '.flow-input-compressed' ).length === 0 ) { + // Form already activated + return $.Deferred().reject(); + } + + component = mw.flow.getPrototypeMethod( 'component', 'getInstanceByElement' )( $( this ) ); + component.logEvent( + 'FlowReplies', + // log data + { + entrypoint: 'new-topic', + action: 'initiate' + }, + // nodes to forward funnel to + $( this ).findWithParent( + '< .flow-newtopic-form [data-role="cancel"],' + + '< .flow-newtopic-form [data-role="action"][name="preview"],' + + '< .flow-newtopic-form [data-role="submit"]' + ) + ); + + return $.Deferred().resolve(); + }; + + /** + * @param {Event} event + */ FlowBoardComponentInteractiveEventsMixin.UI.events.interactiveHandlers.activateReplyPost = function ( event ) { event.preventDefault(); diff --git a/modules/engine/components/board/base/flow-board-misc.js b/modules/engine/components/board/base/flow-board-misc.js index 99043c2..dca5813 100644 --- a/modules/engine/components/board/base/flow-board-misc.js +++ b/modules/engine/components/board/base/flow-board-misc.js @@ -93,6 +93,80 @@ } FlowBoardComponentMiscMixin.prototype.resetPreview = flowBoardComponentResetPreview; + /** + * This will trigger an eventLog call to the given schema with the given + * parameters (along with other info about the user & page.) + * A unique funnel ID will be created for all new EventLog calls. + * + * There may be multiple subsequent calls in the same "funnel" (and share + * same info) that you want to track. It is possible to forward funnel data + * from one node to another once the first has been clicked. It'll then + * log new calls with the same data (schema & entrypoint) & funnel ID as the + * initial logged event. + * + * @param {string} schemaName + * @param {object} data Data to be logged + * @param {string} data.action Schema's action parameter. Always required! + * @param {string} [data.entrypoint] Schema's entrypoint parameter (can be + * omitted if already logged in funnel - will inherit) + * @param {string} [data.funnelId] Schema's funnelId parameter (can be + * omitted if starting new funnel - will be generated) + * @param {jQuery} [$forward] Nodes to forward funnel to + * @returns {object} Logged data + */ + function logEvent( schemaName, data, $forward ) { + var // Get existing (forwarded) funnel id, or generate a new one if it does not yet exist + funnelId = data.funnelId || mw.flow.FlowEventLogRegistry.generateFunnelId(), + // Fetch existing EventLog object for this funnel (if any) + eventLog = mw.flow.FlowEventLogRegistry.funnels[funnelId]; + + // Optional argument, may not want/need to forward funnel to other nodes + $forward = $forward || $(); + + if ( !eventLog ) { + // Add some more data to log! + data = $.extend( data, { + isAnon: mw.user.isAnon(), + sessionId: mw.user.sessionId(), + funnelId: funnelId, + pageNs: mw.config.get( 'wgNamespaceNumber' ), + pageTitle: ( new mw.Title( mw.config.get( 'wgPageName' ) ) ).getMain() + } ); + + // A funnel with this id does not yet exist, create it! + eventLog = new mw.flow.EventLog( schemaName, data ); + + // Store this particular eventLog - we may want to log more things + // in this funnel + mw.flow.FlowEventLogRegistry.funnels[funnelId] = eventLog; + } + + // Log this action + eventLog.logEvent( { action: data.action } ); + + // Forward the event + this.forwardEvent( $forward, schemaName, funnelId ); + + return data; + } + FlowBoardComponentMiscMixin.prototype.logEvent = logEvent; + + /** + * Forward funnel data to other places. + * + * @param {jQuery} $forward Nodes to forward funnel to + * @param {string} schemaName + * @param {string} funnelId Schema's funnelId parameter + */ + function forwardEvent( $forward, schemaName, funnelId ) { + // Not using data() - it somehow gets lost on some nodes + $forward.attr( { + 'data-flow-eventlog-schema': schemaName, + 'data-flow-eventlog-funnel-id': funnelId + } ); + } + FlowBoardComponentMiscMixin.prototype.forwardEvent = forwardEvent; + // // Static functions // @@ -232,4 +306,4 @@ // Mixin to FlowBoardComponent mw.flow.mixinComponent( 'board', FlowBoardComponentMiscMixin ); -}( jQuery, mediaWiki ) ); \ No newline at end of file +}( jQuery, mediaWiki ) ); diff --git a/modules/engine/components/board/base/flow-boardandhistory-base.js b/modules/engine/components/board/base/flow-boardandhistory-base.js index 05185ba..f27991c 100644 --- a/modules/engine/components/board/base/flow-boardandhistory-base.js +++ b/modules/engine/components/board/base/flow-boardandhistory-base.js @@ -115,9 +115,18 @@ flowComponent = mw.flow.getPrototypeMethod( 'boardAndHistoryBase', 'getInstanceByElement' )( $form ), $fields = $form.find( 'textarea, :text' ), changedFieldCount = 0, - $deferred = $.Deferred(); + $deferred = $.Deferred(), + callbacks = $form.data( 'flow-cancel-callback' ) || [], + schemaName = $( this ).data( 'flow-eventlog-schema' ), + funnelId = $( this ).data( 'flow-eventlog-funnel-id' ); event.preventDefault(); + + // Only log cancel attempt if it was user-initiated, not when the cancel + // was triggered by code (as part of a post-submit form destroy) + if ( event.which ) { + flowComponent.logEvent( schemaName, { action: 'cancel-attempt', funnelId: funnelId } ); + } // Check for non-empty fields of text $fields.each( function () { @@ -127,31 +136,36 @@ } } ); - // If all the text fields are empty, OR if the user confirms to close this with text already entered, do it. - if ( !changedFieldCount || confirm( flowComponent.constructor.static.TemplateEngine.l10n( 'flow-cancel-warning' ) ) ) { - // Reset the form content - $form[0].reset(); + // Only log if user had already entered text (= confirmation was requested) + if ( changedFieldCount ) { + if ( confirm( flowComponent.constructor.static.TemplateEngine.l10n( 'flow-cancel-warning' ) ) ) { + flowComponent.logEvent( schemaName, { action: 'cancel-success', funnelId: funnelId } ); + } else { + flowComponent.logEvent( schemaName, { action: 'cancel-abort', funnelId: funnelId } ); - // Trigger for flow-actions-disabler - $form.find( 'textarea, :text' ).trigger( 'keyup' ); - - // Hide the form - flowComponent.emitWithReturn( 'hideForm', $form ); - - // Get rid of existing error messages - flowComponent.emitWithReturn( 'removeError', $form ); - - // Trigger the cancel callback - if ( $form.data( 'flow-cancel-callback' ) ) { - $.each( $form.data( 'flow-cancel-callback' ), function ( idx, fn ) { - fn.call( target, event ); - } ); + // User aborted cancel, quit this function & don't destruct the form! + return $deferred.reject().promise(); } - - return $deferred.resolve().promise(); } - return $deferred.reject().promise(); + // Reset the form content + $form[0].reset(); + + // Trigger for flow-actions-disabler + $form.find( 'textarea, :text' ).trigger( 'keyup' ); + + // Hide the form + flowComponent.emitWithReturn( 'hideForm', $form ); + + // Get rid of existing error messages + flowComponent.emitWithReturn( 'removeError', $form ); + + // Trigger the cancel callback + $.each( callbacks, function ( idx, fn ) { + fn.call( target, event ); + } ); + + return $deferred.resolve().promise(); }; // diff --git a/modules/engine/components/common/flow-component-engines.js b/modules/engine/components/common/flow-component-engines.js index 2769efd..3d8a928 100644 --- a/modules/engine/components/common/flow-component-engines.js +++ b/modules/engine/components/common/flow-component-engines.js @@ -28,6 +28,12 @@ */ mw.flow.Api = new mw.flow.FlowApi( FlowComponentEnginesMixin.static.StorageEngine ); + /** + * EventLogging wrapper + * @type {FlowEventLog} + */ + mw.flow.EventLog = mw.flow.FlowEventLog; + // Copy static and prototype from mixin to main class mw.flow.mixinComponent( 'component', FlowComponentEnginesMixin ); }( jQuery, mediaWiki, mediaWiki.flow.vendor.initStorer ) ); diff --git a/modules/engine/components/common/flow-component-events.js b/modules/engine/components/common/flow-component-events.js index 05c730e..f5edc41 100644 --- a/modules/engine/components/common/flow-component-events.js +++ b/modules/engine/components/common/flow-component-events.js @@ -59,6 +59,11 @@ 'focusin.FlowBoardComponent', 'input.mw-ui-input, textarea', _getDispatchCallback( this, 'focusField' ) + ) + .on( + 'click.FlowBoardComponent keypress.FlowBoardComponent', + '[data-flow-eventlog-action]', + _getDispatchCallback( this, 'eventLogHandler' ) ); if ( _isGlobalBound ) { @@ -319,6 +324,10 @@ // Remove existing errors from previous attempts flowComponent.emitWithReturn( 'removeError', $this ); + // We'll return a deferred object that won't resolve before apiHandlers + // are resolved + $handlerDeferred = $.Deferred(); + // If this has a special api handler, bind it to the callback. if ( flowComponent.UI.events.apiHandlers[ handlerName ] ) { $deferred @@ -468,6 +477,8 @@ this.debug( 'Failed to find apiHandler', apiHandlerName, arguments ); } + // Add aggregate deferred object as data attribute, so we can hook into + // the element when the handlers have run $context.data( 'flow-interactive-handler-promise', $.when.apply( $, promises ) ); } @@ -475,7 +486,7 @@ * Triggers both API and interactive handlers, on click/enter. */ function flowInteractiveHandlerCallback( event ) { - // Only trigger with enter key, if keypress + // Only trigger with enter key & no modifier keys, if keypress if ( event.type === 'keypress' && ( event.charCode !== 13 || event.metaKey || event.shiftKey || event.ctrlKey || event.altKey )) { return; } @@ -504,6 +515,68 @@ FlowComponentEventsMixin.eventHandlers.interactiveHandlerFocus = flowInteractiveHandlerFocusCallback; /** + * Callback function for when a [data-flow-eventlog-action] node is clicked. + * This will trigger a eventLog call to the given schema with the given + * parameters. + * A unique funnel ID will be created for all new EventLog calls. + * + * There may be multiple subsequent calls in the same "funnel" (and share + * same info) that you want to track. It is possible to forward funnel data + * from one attribute to another once the first has been clicked. It'll then + * log new calls with the same data (schema & entrypoint) & funnel ID as the + * initial logged event. + * + * Required parameters (as data-attributes) are: + * * data-flow-eventlog-schema: The schema name + * * data-flow-eventlog-entrypoint: The schema's entrypoint parameter + * * data-flow-eventlog-action: The schema's action parameter + * + * Additionally: + * * data-flow-eventlog-forward: Selectors to forward funnel data to + */ + function flowEventLogCallback( event ) { + // Only trigger with enter key & no modifier keys, if keypress + if ( event.type === 'keypress' && ( event.charCode !== 13 || event.metaKey || event.shiftKey || event.ctrlKey || event.altKey )) { + return; + } + + var $context = $( event.currentTarget ), + data = $context.data(), + component = mw.flow.getPrototypeMethod( 'component', 'getInstanceByElement' )( $context ), + $promise = data.flowInteractiveHandlerPromise || $.Deferred().resolve().promise(), + eventInstance = {}, + key, value; + + // Fetch loggable data: everything prefixed flowEventlog + for ( key in data ) { + if ( key.indexOf( 'flowEventlog' ) === 0 ) { + // Strips "flowEventlog" and lowercases first char after that + value = data[key]; + key = key.substr( 12, 1 ).toLowerCase() + key.substr( 13 ); + + eventInstance[key] = value; + } + } + + // Forward is not loggable data! + delete eventInstance.forward; + + // Log the event + eventInstance = component.logEvent( data.flowEventlogSchema, eventInstance ); + + // Promise resolves once all interactiveHandlers/apiHandlers are done, + // so all nodes we want to forward to are bound to be there + $promise.always( function() { + // Now find all nodes to forward to + var $forward = data.flowEventlogForward ? $context.findWithParent( data.flowEventlogForward ) : $(); + + // Forward the funnel + eventInstance = component.forwardEvent( $forward, data.flowEventlogSchema, eventInstance.funnelId ); + } ); + } + FlowComponentEventsMixin.eventHandlers.eventLogHandler = flowEventLogCallback; + + /** * When the whole class has been instantiated fully (after every constructor has been called). * @param {FlowComponent} component */ diff --git a/modules/engine/misc/flow-eventlog.js b/modules/engine/misc/flow-eventlog.js new file mode 100644 index 0000000..aafa751 --- /dev/null +++ b/modules/engine/misc/flow-eventlog.js @@ -0,0 +1,45 @@ +( function ( mw, $ ) { + /** + * @param {String} schemaName Canonical schema name. + * @param {Object} [eventInstance] Shared event instance data. + * @returns {FlowEventLog} + * @constructor + */ + function FlowEventLog( schemaName, eventInstance ) { + this.schemaName = schemaName; + this.eventInstance = eventInstance || {}; + + /** + * @param {object} eventInstance Additional event instance data for this + * particular event. + * @returns {$.Deferred} + */ + function logEvent( eventInstance ) { + // Ensure eventLog & this schema exist, or return a stub deferred + if ( !mw.eventLog || !mw.eventLog.schemas[this.schemaName] ) { + return $.Deferred().promise(); + } + + return mw.eventLog.logEvent( + this.schemaName, + $.extend( this.eventInstance, eventInstance ) + ); + } + this.logEvent = logEvent; + } + + var FlowEventLogRegistry = { + funnels: {}, + + /** + * Generates a unique id. + * + * @returns {string} + */ + generateFunnelId: mw.user.generateRandomSessionId + }; + + // Export + mw.flow.FlowEventLog = FlowEventLog; + mw.flow.FlowEventLogRegistry = FlowEventLogRegistry; +}( mw, jQuery ) ); -- To view, visit https://gerrit.wikimedia.org/r/171791 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ic011a6130184aadae539eced307bf982f84c990d Gerrit-PatchSet: 20 Gerrit-Project: mediawiki/extensions/Flow Gerrit-Branch: master Gerrit-Owner: Matthias Mullie <[email protected]> Gerrit-Reviewer: EBernhardson <[email protected]> Gerrit-Reviewer: Mattflaschen <[email protected]> Gerrit-Reviewer: Matthias Mullie <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
