Procházet zdrojové kódy

Add locale indication to the React page explorer

Karl Hobley před 4 roky
rodič
revize
123d9cef92

+ 1 - 0
client/src/api/admin.ts

@@ -15,6 +15,7 @@ export interface WagtailPageAPI {
     parent: {
       id: number;
     } | null;
+    locale?: string;
   };
   /* eslint-disable-next-line camelcase */
   admin_display_title?: string;

+ 27 - 1
client/src/components/Explorer/Explorer.scss

@@ -88,6 +88,10 @@ $menu-footer-height: 50px;
     background-color: $c-explorer-bg-dark;
     border-bottom: 1px solid $c-explorer-bg-dark;
     color: $color-white;
+}
+
+.c-explorer__header__title {
+    color: inherit;
 
     &:focus {
         background-color: $c-explorer-bg-active;
@@ -104,7 +108,9 @@ $menu-footer-height: 50px;
     }
 }
 
-.c-explorer__header__inner {
+.c-explorer__header__title__inner {
+    width: 70%;
+    float: left;
     padding: 1em 0.75em;
     overflow: hidden;
     text-overflow: ellipsis;
@@ -129,6 +135,26 @@ $menu-footer-height: 50px;
     }
 }
 
+.c-explorer__header__select {
+    float: right;
+    position: relative;
+    padding: 1em 0;
+    padding-right: 2em;
+    text-align: right;
+
+    span {
+        background-color: $c-explorer-bg-active;
+        display: inline-block;
+        padding: 0.2em 0.5em;
+        border-radius: 0.25em;
+        vertical-align: middle;
+        line-height: 1.5;
+        text-transform: uppercase;
+        letter-spacing: 0.03rem;
+        font-size: 12px;
+    }
+}
+
 .c-explorer__placeholder {
     padding: 1em;
     color: $color-white;

+ 22 - 14
client/src/components/Explorer/ExplorerHeader.tsx

@@ -1,7 +1,7 @@
 /* eslint-disable react/prop-types */
 
 import React from 'react';
-import { ADMIN_URLS, STRINGS } from '../../config/wagtailConfig';
+import { ADMIN_URLS, STRINGS, LOCALE_NAMES } from '../../config/wagtailConfig';
 
 import Button from '../../components/Button/Button';
 import Icon from '../../components/Icon/Icon';
@@ -19,21 +19,29 @@ interface ExplorerHeaderProps {
  */
 const ExplorerHeader: React.FunctionComponent<ExplorerHeaderProps> = ({ page, depth, onClick }) => {
   const isRoot = depth === 0;
+  const isSiteRoot = page.id === 0;
 
   return (
-    <Button
-      href={page.id ? `${ADMIN_URLS.PAGES}${page.id}/` : ADMIN_URLS.PAGES}
-      className="c-explorer__header"
-      onClick={onClick}
-    >
-      <div className="c-explorer__header__inner">
-        <Icon
-          name={isRoot ? 'home' : 'arrow-left'}
-          className="icon--explorer-header"
-        />
-        <span>{page.admin_display_title || STRINGS.PAGES}</span>
-      </div>
-    </Button>
+    <div className="c-explorer__header">
+      <Button
+        href={!isSiteRoot ? `${ADMIN_URLS.PAGES}${page.id}/` : ADMIN_URLS.PAGES}
+        className="c-explorer__header__title "
+        onClick={onClick}
+      >
+        <div className="c-explorer__header__title__inner ">
+          <Icon
+            name={isRoot ? 'home' : 'arrow-left'}
+            className="icon--explorer-header"
+          />
+          <span>{page.admin_display_title || STRINGS.PAGES}</span>
+        </div>
+      </Button>
+      {!isSiteRoot && page.meta.locale &&
+        <div className="c-explorer__header__select">
+          <span>{(LOCALE_NAMES.get(page.meta.locale) || page.meta.locale)}</span>
+        </div>
+      }
+    </div>
   );
 };
 

+ 6 - 4
client/src/components/Explorer/ExplorerItem.tsx

@@ -2,7 +2,7 @@
 
 import React from 'react';
 
-import { ADMIN_URLS, STRINGS } from '../../config/wagtailConfig';
+import { ADMIN_URLS, STRINGS, LOCALE_NAMES } from '../../config/wagtailConfig';
 import Icon from '../../components/Icon/Icon';
 import Button from '../../components/Button/Button';
 import PublicationStatus from '../../components/PublicationStatus/PublicationStatus';
@@ -26,6 +26,7 @@ const ExplorerItem: React.FunctionComponent<ExplorerItemProps> = ({ item, onClic
   const { id, admin_display_title: title, meta } = item;
   const hasChildren = meta.children.count > 0;
   const isPublished = meta.status.live && !meta.status.has_unpublished_changes;
+  const localeName = meta.parent?.id === 1 && meta.locale && (LOCALE_NAMES.get(meta.locale) || meta.locale);
 
   return (
     <div className="c-explorer__item">
@@ -36,11 +37,12 @@ const ExplorerItem: React.FunctionComponent<ExplorerItemProps> = ({ item, onClic
           {title}
         </h3>
 
-        {!isPublished ? (
+        {(!isPublished || localeName) &&
           <span className="c-explorer__meta">
-            <PublicationStatus status={meta.status} />
+            {localeName && <span className="o-pill c-status">{localeName}</span>}
+            {!isPublished && <PublicationStatus status={meta.status} />}
           </span>
-        ) : null}
+        }
       </Button>
       <Button
         href={`${ADMIN_URLS.PAGES}${id}/edit/`}

+ 1 - 1
client/src/components/Explorer/ExplorerPanel.tsx

@@ -162,7 +162,7 @@ class ExplorerPanel extends React.Component<ExplorerPanelProps, ExplorerPanelSta
         className="explorer"
         paused={paused || !page || page.isFetching}
         focusTrapOptions={{
-          initialFocus: '.c-explorer__header',
+          initialFocus: '.c-explorer__header__title',
           onDeactivate: onClose,
         }}
       >

+ 72 - 60
client/src/components/Explorer/__snapshots__/ExplorerHeader.test.js.snap

@@ -1,79 +1,91 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ExplorerHeader #depth at root 1`] = `
-<Button
-  accessibleLabel={null}
+<div
   className="c-explorer__header"
-  dialogTrigger={false}
-  href="/admin/pages/"
-  isLoading={false}
-  onClick={[MockFunction]}
-  preventDefault={true}
-  target={null}
 >
-  <div
-    className="c-explorer__header__inner"
+  <Button
+    accessibleLabel={null}
+    className="c-explorer__header__title "
+    dialogTrigger={false}
+    href="/admin/pages/undefined/"
+    isLoading={false}
+    onClick={[MockFunction]}
+    preventDefault={true}
+    target={null}
   >
-    <Icon
-      className="icon--explorer-header"
-      name="home"
-      title={null}
-    />
-    <span>
-      Pages
-    </span>
-  </div>
-</Button>
+    <div
+      className="c-explorer__header__title__inner "
+    >
+      <Icon
+        className="icon--explorer-header"
+        name="home"
+        title={null}
+      />
+      <span>
+        Pages
+      </span>
+    </div>
+  </Button>
+</div>
 `;
 
 exports[`ExplorerHeader #page 1`] = `
-<Button
-  accessibleLabel={null}
+<div
   className="c-explorer__header"
-  dialogTrigger={false}
-  href="/admin/pages/a/"
-  isLoading={false}
-  onClick={[MockFunction]}
-  preventDefault={true}
-  target={null}
 >
-  <div
-    className="c-explorer__header__inner"
+  <Button
+    accessibleLabel={null}
+    className="c-explorer__header__title "
+    dialogTrigger={false}
+    href="/admin/pages/a/"
+    isLoading={false}
+    onClick={[MockFunction]}
+    preventDefault={true}
+    target={null}
   >
-    <Icon
-      className="icon--explorer-header"
-      name="arrow-left"
-      title={null}
-    />
-    <span>
-      test
-    </span>
-  </div>
-</Button>
+    <div
+      className="c-explorer__header__title__inner "
+    >
+      <Icon
+        className="icon--explorer-header"
+        name="arrow-left"
+        title={null}
+      />
+      <span>
+        test
+      </span>
+    </div>
+  </Button>
+</div>
 `;
 
 exports[`ExplorerHeader basic 1`] = `
-<Button
-  accessibleLabel={null}
+<div
   className="c-explorer__header"
-  dialogTrigger={false}
-  href="/admin/pages/"
-  isLoading={false}
-  onClick={[MockFunction]}
-  preventDefault={true}
-  target={null}
 >
-  <div
-    className="c-explorer__header__inner"
+  <Button
+    accessibleLabel={null}
+    className="c-explorer__header__title "
+    dialogTrigger={false}
+    href="/admin/pages/undefined/"
+    isLoading={false}
+    onClick={[MockFunction]}
+    preventDefault={true}
+    target={null}
   >
-    <Icon
-      className="icon--explorer-header"
-      name="arrow-left"
-      title={null}
-    />
-    <span>
-      Pages
-    </span>
-  </div>
-</Button>
+    <div
+      className="c-explorer__header__title__inner "
+    >
+      <Icon
+        className="icon--explorer-header"
+        name="arrow-left"
+        title={null}
+      />
+      <span>
+        Pages
+      </span>
+    </div>
+  </Button>
+</div>
 `;

+ 5 - 5
client/src/components/Explorer/__snapshots__/ExplorerPanel.test.js.snap

@@ -7,7 +7,7 @@ exports[`ExplorerPanel general rendering #isError 1`] = `
   className="explorer"
   focusTrapOptions={
     Object {
-      "initialFocus": ".c-explorer__header",
+      "initialFocus": ".c-explorer__header__title",
       "onDeactivate": [MockFunction],
     }
   }
@@ -91,7 +91,7 @@ exports[`ExplorerPanel general rendering #isFetching 1`] = `
   className="explorer"
   focusTrapOptions={
     Object {
-      "initialFocus": ".c-explorer__header",
+      "initialFocus": ".c-explorer__header__title",
       "onDeactivate": [MockFunction],
     }
   }
@@ -162,7 +162,7 @@ exports[`ExplorerPanel general rendering #items 1`] = `
   className="explorer"
   focusTrapOptions={
     Object {
-      "initialFocus": ".c-explorer__header",
+      "initialFocus": ".c-explorer__header__title",
       "onDeactivate": [MockFunction],
     }
   }
@@ -255,7 +255,7 @@ exports[`ExplorerPanel general rendering no children 1`] = `
   className="explorer"
   focusTrapOptions={
     Object {
-      "initialFocus": ".c-explorer__header",
+      "initialFocus": ".c-explorer__header__title",
       "onDeactivate": [MockFunction],
     }
   }
@@ -317,7 +317,7 @@ exports[`ExplorerPanel general rendering renders 1`] = `
   className="explorer"
   focusTrapOptions={
     Object {
-      "initialFocus": ".c-explorer__header",
+      "initialFocus": ".c-explorer__header__title",
       "onDeactivate": [MockFunction],
     }
   }

+ 7 - 0
client/src/config/wagtailConfig.js

@@ -6,3 +6,10 @@ export const ADMIN_URLS = global.wagtailConfig.ADMIN_URLS;
 export const MAX_EXPLORER_PAGES = 200;
 
 export const IS_IE11 = !global.ActiveXObject && 'ActiveXObject' in global;
+
+export const LOCALE_NAMES = new Map();
+
+/* eslint-disable-next-line camelcase */
+global.wagtailConfig.LOCALES.forEach(({ code, display_name }) => {
+  LOCALE_NAMES.set(code, display_name);
+});

+ 10 - 0
client/src/custom.d.ts

@@ -2,4 +2,14 @@ export {};
 
 declare global {
     interface Window { __REDUX_DEVTOOLS_EXTENSION__: any; }
+
+    interface WagtailConfig {
+        I18N_ENABLED: boolean;
+        LOCALES: {
+            code: string;
+            /* eslint-disable-next-line camelcase */
+            display_name: string;
+        }[];
+    }
+    const wagtailConfig: WagtailConfig;
 }

+ 12 - 0
client/tests/stubs.js

@@ -47,6 +47,18 @@ global.wagtailConfig = {
     VIEW_CHILD_PAGES_OF_PAGE: 'View child pages of \'{title}\'',
     PAGE_EXPLORER: 'Page explorer',
   },
+  WAGTAIL_I18N_ENABLED: true,
+  LOCALES: [
+    {
+      code: 'en',
+      display_name: 'English'
+    },
+    {
+      code: 'fr',
+      display_nam: 'French'
+    }
+  ],
+  ACTIVE_LOCALE: 'en'
 };
 
 global.wagtailVersion = '1.6a1';

+ 5 - 0
wagtail/admin/templates/wagtailadmin/admin_base.html

@@ -27,6 +27,11 @@
                 EXTRA_CHILDREN_PARAMETERS: '',
             };
 
+            {% i18n_enabled as i18n_enabled %}
+            {% locales as locales %}
+            wagtailConfig.I18N_ENABLED = {% if i18n_enabled %}true{% else %}false{% endif %};
+            wagtailConfig.LOCALES = {{ locales|safe }};
+
             wagtailConfig.STRINGS = {% js_translation_strings %};
 
             wagtailConfig.ADMIN_URLS = {

+ 18 - 1
wagtail/admin/templatetags/wagtailadmin_tags.py

@@ -14,6 +14,7 @@ from django.template.defaultfilters import stringfilter
 from django.template.loader import render_to_string
 from django.templatetags.static import static
 from django.utils import timezone
+from django.utils.encoding import force_str
 from django.utils.html import avoid_wrapping, format_html, format_html_join
 from django.utils.safestring import mark_safe
 from django.utils.timesince import timesince
@@ -27,7 +28,7 @@ from wagtail.admin.search import admin_search_areas
 from wagtail.admin.staticfiles import versioned_static as versioned_static_func
 from wagtail.core import hooks
 from wagtail.core.models import (
-    Collection, CollectionViewRestriction, Page, PageLogEntry, PageViewRestriction,
+    Collection, CollectionViewRestriction, Locale, Page, PageLogEntry, PageViewRestriction,
     UserPagePermissionsProxy)
 from wagtail.core.utils import accepts_kwarg, camelcase_to_underscore
 from wagtail.core.utils import cautious_slugify as _cautious_slugify
@@ -637,3 +638,19 @@ def user_display_name(user):
         # we were passed None or something else that isn't a valid user object; return
         # empty string to replicate the behaviour of {{ user.get_full_name|default:user.get_username }}
         return ''
+
+
+@register.simple_tag
+def i18n_enabled():
+    return getattr(settings, 'WAGTAIL_I18N_ENABLED', False)
+
+
+@register.simple_tag
+def locales():
+    return json.dumps([
+        {
+            'code': locale.language_code,
+            'display_name': force_str(locale.get_display_name()),
+        }
+        for locale in Locale.objects.all()
+    ])

+ 8 - 5
wagtail/snippets/tests.py

@@ -184,7 +184,7 @@ class TestLocaleSelectorOnList(TestCase, WagtailTestUtils):
             reverse('wagtailsnippets:list', args=['tests', 'advert'])
         )
 
-        self.assertNotContains(response, 'French')
+        self.assertNotContains(response, 'aria-label="French" class="u-link is-live">')
 
         # Check that the add URLs don't include the locale
         add_url = reverse('wagtailsnippets:add', args=['tests', 'advert'])
@@ -715,6 +715,9 @@ class TestEditFileUploadSnippet(BaseTestSnippetEditView):
 class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
     fixtures = ['test.json']
 
+    LOCALE_SELECTOR_HTML = '<a href="javascript:void(0)" aria-label="English" class="c-dropdown__button  u-btn-current">'
+    LOCALE_INDICATOR_HTML = '<use href="#icon-site"></use></svg>\n    English'
+
     def setUp(self):
         super().setUp()
         self.test_snippet = TranslatableSnippet.objects.create(text="This is a test")
@@ -725,7 +728,7 @@ class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
     def test_locale_selector(self):
         response = self.get()
 
-        self.assertContains(response, 'English')
+        self.assertContains(response, self.LOCALE_SELECTOR_HTML)
 
         switch_to_french_url = reverse('wagtailsnippets:edit', args=['snippetstests', 'translatablesnippet', quote(self.test_snippet_fr.pk)])
         self.assertContains(response, f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live">')
@@ -735,7 +738,7 @@ class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
 
         response = self.get()
 
-        self.assertContains(response, 'English')
+        self.assertContains(response, self.LOCALE_INDICATOR_HTML)
 
         switch_to_french_url = reverse('wagtailsnippets:edit', args=['snippetstests', 'translatablesnippet', quote(self.test_snippet_fr.pk)])
         self.assertNotContains(response, f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live">')
@@ -744,7 +747,7 @@ class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
     def test_locale_selector_not_present_when_i18n_disabled(self):
         response = self.get()
 
-        self.assertNotContains(response, 'English')
+        self.assertNotContains(response, self.LOCALE_SELECTOR_HTML)
 
         switch_to_french_url = reverse('wagtailsnippets:edit', args=['snippetstests', 'translatablesnippet', quote(self.test_snippet_fr.pk)])
         self.assertNotContains(response, f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live">')
@@ -754,7 +757,7 @@ class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
 
         response = self.get()
 
-        self.assertNotContains(response, 'English')
+        self.assertNotContains(response, self.LOCALE_SELECTOR_HTML)
         self.assertNotContains(response, 'aria-label="French" class="u-link is-live">')