diff --git a/package-lock.json b/package-lock.json index 23a7adf..01d8407 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1546,6 +1546,15 @@ "hoist-non-react-statics": "^3.3.0" } }, + "@types/jquery": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.4.tgz", + "integrity": "sha512-//9CHhaUt/rurMJTxGI+I6DmsNHgYU6d8aSLFfO5dB7+10lwLnaWT0z5GY/yY82Q/M+B+0Qh3TixlJ8vmBeqIw==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, "@types/json-schema": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", @@ -1604,6 +1613,12 @@ "@types/react": "*" } }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", + "dev": true + }, "@types/styled-components": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.4.tgz", diff --git a/package.json b/package.json index acbc394..476630c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "@svgr/webpack": "^5.4.0", "@types/c3": "^0.7.5", + "@types/jquery": "^3.5.4", "@types/react": "^16.8.19", "@types/react-dom": "^16.8.19", "@types/styled-components": "^5.1.2", diff --git a/wagtail_ab_testing/compat.py b/wagtail_ab_testing/compat.py new file mode 100644 index 0000000..97a5f07 --- /dev/null +++ b/wagtail_ab_testing/compat.py @@ -0,0 +1,8 @@ +import os + +if os.name == 'nt': + # Windows has a different strftime format for dates without leading 0 + # https://stackoverflow.com/questions/904928/python-strftime-date-without-leading-0 + DATE_FORMAT = '%#d %B %Y' +else: + DATE_FORMAT = '%-d %B %Y' diff --git a/wagtail_ab_testing/static_src/components/PageEditorTab/index.tsx b/wagtail_ab_testing/static_src/components/PageEditorTab/index.tsx new file mode 100644 index 0000000..8008fc0 --- /dev/null +++ b/wagtail_ab_testing/static_src/components/PageEditorTab/index.tsx @@ -0,0 +1,46 @@ +import React, { FunctionComponent } from 'react'; +import ReactDOM from 'react-dom'; + +interface AbTest { + id: number; + name: string; + started_at: string; + status: string; +} + +interface PageEditorTabProps { + tests: AbTest[]; +} + +const PageEditorTab: FunctionComponent = ({ tests }) => { + return ( +
+ + + + + + + + + + {tests.map(test => ( + + + + + + ))} + +
{gettext('Started at')}{gettext('Test name')}{gettext('Status')}
{test.started_at}{test.name} + + {test.status} + +
+
+ ); +}; + +export function initPageEditorTab(element: HTMLElement, props: any) { + ReactDOM.render(, element); +} diff --git a/wagtail_ab_testing/static_src/custom.d.ts b/wagtail_ab_testing/static_src/custom.d.ts index e741cbf..dc2301b 100644 --- a/wagtail_ab_testing/static_src/custom.d.ts +++ b/wagtail_ab_testing/static_src/custom.d.ts @@ -9,6 +9,8 @@ declare module '*.svg' { // Declare globals provided by Django's JavaScript Catalog // For more information, see: https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#module-django.views.i18n declare global { + const abTestingTabProps: any | undefined; + // Wagtail globals interface WagtailConfig { diff --git a/wagtail_ab_testing/static_src/main.tsx b/wagtail_ab_testing/static_src/main.tsx index c2abe3b..e6dfa08 100644 --- a/wagtail_ab_testing/static_src/main.tsx +++ b/wagtail_ab_testing/static_src/main.tsx @@ -1,14 +1,17 @@ import c3 from 'c3'; import { initGoalSelector } from './components/GoalSelector'; +import { initPageEditorTab } from './components/PageEditorTab'; import './style/progress.scss'; import './styles/sections.scss'; import './styles/forms.scss'; document.addEventListener('DOMContentLoaded', () => { + // Goal selector on create new A/B test initGoalSelector(); + // Charts on A/B test progress document.querySelectorAll('[component="chart"]').forEach(chartElement => { if ( !(chartElement instanceof HTMLElement) || @@ -36,4 +39,20 @@ document.addEventListener('DOMContentLoaded', () => { } }); }); + + // A/B testing tab on page edito + if (abTestingTabProps) { + $('ul.tab-nav').append(`
  • + ${gettext('A/B testing')} +
  • `); + $('div.tab-content').append(` +
    +
    + `); + + const abTestingTab = document.getElementById('tab-abtesting'); + if (abTestingTab) { + initPageEditorTab(abTestingTab, abTestingTabProps); + } + } }); diff --git a/wagtail_ab_testing/wagtail_hooks.py b/wagtail_ab_testing/wagtail_hooks.py index 0090f6a..1c59bc1 100644 --- a/wagtail_ab_testing/wagtail_hooks.py +++ b/wagtail_ab_testing/wagtail_hooks.py @@ -1,13 +1,18 @@ +import json + from django.shortcuts import redirect from django.urls import path, include, reverse +from django.utils.html import format_html, escapejs from django.utils.translation import gettext as _, gettext_lazy as __ from django.views.i18n import JavaScriptCatalog from wagtail.admin.action_menu import ActionMenuItem from wagtail.admin.menu import MenuItem +from wagtail.admin.staticfiles import versioned_static from wagtail.core import hooks from . import views +from .compat import DATE_FORMAT from .models import AbTest from .utils import request_is_trackable @@ -49,6 +54,35 @@ def register_create_abtest_action_menu_item(): return CreateAbTestActionMenuItem(order=100) +# This is the only way to inject custom JS into the editor with knowledge of the page being edited +class AbTestingTabActionMenuItem(ActionMenuItem): + def render_html(self, request, context): + if 'page' in context: + return format_html( + '', + reverse('wagtail_ab_testing:javascript_catalog'), + versioned_static('wagtail_ab_testing/js/wagtail-ab-testing.js'), + escapejs(json.dumps({ + 'tests': [ + { + 'id': ab_test.id, + 'name': ab_test.name, + 'started_at': ab_test.first_started_at.strftime(DATE_FORMAT) if ab_test.first_started_at else _("Not started"), + 'status': ab_test.get_status_description(), + } + for ab_test in AbTest.objects.filter(page=context['page']).order_by('-id') + ] + })) + ) + + return '' + + +@hooks.register('register_page_action_menu_item') +def register_ab_testing_tab_action_menu_item(): + return AbTestingTabActionMenuItem() + + @hooks.register('after_edit_page') def redirect_to_create_ab_test(request, page): if 'create-ab-test' in request.POST: