Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/olympia/devhub/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@
views.addons_section,
name='devhub.addons.section',
),
# Pinguino URL compatibility.
re_path(
r'^edit/authors$',
lambda r, addon_id: redirect('devhub.addons.owner', addon_id),
),
re_path(
r'^edit/[^/]+$',
lambda r, addon_id: redirect('devhub.addons.edit', addon_id),
),
re_path(
r'^feed$',
lambda r, addon_id: redirect('devhub.feed', addon_id),
),
re_path(
r'^statistics$',
lambda r, addon_id: redirect('stats.overview', addon_id),
),
re_path(
r'^upload_preview$',
views.upload_image,
Expand Down
18 changes: 18 additions & 0 deletions src/pinguino/devhub/src/components/coming-soon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';

// Placeholder for devhub pages that exist in the Django app but haven't been
// reimplemented in Pinguino yet, so old bookmarks land somewhere sensible
// instead of a "Not found" page.
@customElement('coming-soon')
export class ComingSoon extends LitElement {
render() {
return html`<p>Coming soon.</p>`;
Comment thread
eviljeff marked this conversation as resolved.
Resolved
}
}

declare global {
interface HTMLElementTagNameMap {
'coming-soon': ComingSoon;
}
}
4 changes: 2 additions & 2 deletions src/pinguino/devhub/src/components/devhub-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { css, html, LitElement, nothing } from 'lit';
import { customElement } from 'lit/decorators.js';
import { BASE } from '../base';
import { profileQuery } from '../data';
import { classicPath } from '../navigation';
import { switchBackPath } from '../switch-back';
import './devhub-account-menu';

const NAV = [
Expand Down Expand Up @@ -62,7 +62,7 @@ export class DevhubHeader extends LitElement {
// cookie; the bare href is a no-JS fallback (view defaults to the index).
#switchToClassic = (event: Event) => {
event.preventDefault();
const path = classicPath(location.pathname, BASE) + location.search;
const path = switchBackPath();
location.href = `${BASE}/switch-pinguino?path=${encodeURIComponent(path)}`;
};

Expand Down
66 changes: 51 additions & 15 deletions src/pinguino/devhub/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ import './foundations/layout';
// After foundations/acorn so it overrides acorn's base.css :root color-scheme.
import './app.css';

import './components/coming-soon';
import './components/devhub-header';
import './components/not-found';
import './pages/devhub-home';
import './pages/addon/main';
import './pages/addon/templates/devhub-addon';

import { BASE } from './base';
import {
interceptOutOfAppLinks,
isDashboardPath,
pageParam,
} from './navigation';

import { navigateTo } from './helper';
import { interceptOutOfAppLinks, isDashboardPath } from './navigation';
import { recordLanding } from './switch-back';

// The Router intercepts same-origin <a> clicks (across shadow DOM) and syncs
// history, so views navigate with plain links instead of manual push/goto.
Expand Down Expand Up @@ -50,21 +50,55 @@ export class PinguinoApp extends LitElement {
private _router = new Router(
this,
[
{ path: `${BASE}/`, render: () => html`<devhub-home></devhub-home>` },
// The developer dashboard lives at the base path (and has themes too).
{
path: `${BASE}{/}?`,
render: () => html`<devhub-home></devhub-home>`,
},
// The old dashboard, themes and /addon(s) URLs redirect to the base.
{
path: `${BASE}/addons`,
enter: () => navigateTo(BASE, { replace: true }),
},
{
path: `${BASE}/themes`,
enter: () => navigateTo(BASE, { replace: true }),
},
// /addon is a typo for /addons, redirect it.
{
path: `${BASE}/addon`,
enter: () => navigateTo(BASE, { replace: true }),
},
// /addons/<id>/... paths are a typo for /addon/<id>/...
{
path: `${BASE}/addons/*`,
enter: () =>
navigateTo(location.pathname.replace('/addons/', '/addon/'), {
replace: true,
}),
},
{
path: `${BASE}/addon/*`,
render: () => html`<addon-routes></addon-routes>`,
},
// The legacy DevHub's per-add-on feed URL, kept working by rendering the
// same add-on shell with the feed tab active.
// feeds now live directly on the dashboard, and per add-on feeds live on the /addon/xxx/feed
{
path: `${BASE}/feed`,
enter: () => navigateTo(BASE, { replace: true }),
},
{
path: `${BASE}/feed/:addonId`,
enter: ({ addonId }) =>
navigateTo(`${BASE}/addon/${addonId}/feed`, { replace: true }),
},
// Top-level pages not yet reimplemented
{
path: `${BASE}/support`,
render: () => html`<coming-soon></coming-soon>`,
},
{
path: `${BASE}/feed/:slug`,
render: ({ slug }) =>
html`<devhub-addon
.slug=${slug ?? ''}
tab="feed"
.page=${pageParam()}
></devhub-addon>`,
path: `${BASE}/verify-email`,
render: () => html`<coming-soon></coming-soon>`,
},
],
{
Expand All @@ -78,6 +112,8 @@ export class PinguinoApp extends LitElement {

connectedCallback() {
super.connectedCallback();
// Remember the url we landed on after redirects, for switching back.
recordLanding();
this.#cleanupLinks = interceptOutOfAppLinks(BASE);
}

Expand Down
14 changes: 0 additions & 14 deletions src/pinguino/devhub/src/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,6 @@ export function isDashboardPath(pathname: string, base: string): boolean {
return pathname === base || pathname === `${base}/`;
}

// Pinguino nests an add-on's pages as tabs under one slug where classic gives
// each its own URL, so a Pinguino path means nothing to classic until it's
// translated — which it needs to be when a developer switches back. Paths with
// no classic counterpart are returned unchanged, so classic resolves or 404s
// them exactly as it does today.
export function classicPath(pathname: string, base: string): string {
if (!isInAppPath(pathname, base)) return pathname;
const segments = pathname.slice(base.length).split('/').filter(Boolean);
if (segments[0] === 'addon' && segments[2] === 'feed') {
return `${base}/feed/${segments[1]}`;
}
return pathname;
}

// Clamped to 1 so a junk or absent value renders the first page, not an empty one.
export function pageParam(search: string = location.search): number {
const page = Number(new URLSearchParams(search).get('page'));
Expand Down
16 changes: 13 additions & 3 deletions src/pinguino/devhub/src/pages/addon/main.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
import { Routes } from '@lit-labs/router';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { BASE } from '../../base';
import { pageParam } from '../../navigation';

import '../../components/coming-soon';
import './submit/main';
import './templates/devhub-addon';
import './versions/main';
import { navigateTo } from '../../helper';

@customElement('addon-routes')
class AddonRoutes extends LitElement {
private _routes = new Routes(this, [
{ path: 'submit/*', render: () => html`<submit-routes></submit-routes>` },
// Top-level add-on pages not yet reimplemented
{ path: 'api/key{/}?', render: () => html`<coming-soon></coming-soon>` },
{ path: 'validate{/}?', render: () => html`<coming-soon></coming-soon>` },
{
path: `:slug`,
render: ({ slug }) =>
html`<devhub-addon .slug=${slug ?? ''}></devhub-addon>`,
enter: ({ slug }) => navigateTo(`${slug}/edit`, { replace: true }),
},
{
path: ':slug/versions/*',
render: ({ slug }) =>
html`<version-routes .slug=${slug ?? ''}></version-routes>`,
},
// Django's /ownership page is the authors section of the edit tab.
{
path: ':slug/ownership',
enter: ({ slug }) =>
navigateTo(`${BASE}/addon/${slug}/edit/authors`, { replace: true }),
},
{
path: `:slug/:tab/:section`,
render: ({ slug, tab, section }) =>
Expand Down
6 changes: 6 additions & 0 deletions src/pinguino/devhub/src/pages/addon/submit/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Routes } from '@lit-labs/router';
import { customElement } from 'lit/decorators.js';

import '../../../components/coming-soon';
import './templates/agreement';

import { html, LitElement } from 'lit';
Expand All @@ -12,6 +13,11 @@ class SubmitRoutes extends LitElement {
path: 'agreement',
render: () => html`<devhub-agreement></devhub-agreement>`,
},
// Theme submission isn't reimplemented yet
{
path: 'theme/agreement{/}?',
Comment thread
eviljeff marked this conversation as resolved.
Resolved
render: () => html`<coming-soon></coming-soon>`,
},
]);
render() {
return this._routes.outlet();
Expand Down
10 changes: 6 additions & 4 deletions src/pinguino/devhub/src/pages/addon/templates/devhub-addon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '../../../components/activity-feed';
import '../../../components/addon-listing';
import '../../../components/addon-manage-header';
import '../../../components/addon-version/addon-version';
import '../../../components/coming-soon';
import { type Addon, addonQuery } from '../../../data';

// Each tab is its own URL under <base>/addon/:slug/<tab>, so a tab is linkable
Expand Down Expand Up @@ -62,6 +63,10 @@ export class DevhubAddon extends LitElement {
};

#panel(addon: Addon) {
// Django detail pages we haven't reimplemented yet
if (this.tab === 'invitation') {
return html`<coming-soon></coming-soon>`;
}
switch (this.#activeTab) {
case 'edit':
return html`<addon-listing
Expand All @@ -79,10 +84,7 @@ export class DevhubAddon extends LitElement {
.section=${this.section || undefined}
></addon-version>`;
default: {
const label = TABS.find((t) => t.value === this.#activeTab)?.label;
return html`<div class="stub">
${label} - coming in a later ticket.
</div>`;
return html`<coming-soon></coming-soon>`;
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/pinguino/devhub/src/pages/addon/versions/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { html, LitElement, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { addonQuery } from '../../../data';

import '../../../components/coming-soon';
import '../../../components/not-found';
import './submit/main';

Expand All @@ -18,6 +19,7 @@ class VersionRoutes extends LitElement {
render: () =>
html`<version-submit-routes .slug=${this.slug}></version-submit-routes>`,
},
{ path: ':versionId', render: () => html`<coming-soon></coming-soon>` },
]);

render() {
Expand Down
31 changes: 31 additions & 0 deletions src/pinguino/devhub/src/switch-back.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Tracks which URL the "switch back to the classic DevHub" link should restore.
//
// When we first load we may redirect (e.g. /developers/themes -> /developers).
// We want switching back to return to the *original* incoming URL, not the
// redirected one — while the user is still on the page that redirect landed on.

function currentUrl(): string {
return location.pathname + location.search;
}

// Capture the first load url at module-eval time, before client-side redirects.
const initialUrl = currentUrl();

// The URL the initial redirect(s) settled on, recorded once the app has mounted.
let landingUrl: string | null = null;

export function recordLanding(): void {
landingUrl = currentUrl();
}

export function pickSwitchBackPath(
initial: string,
landing: string | null,
current: string,
): string {
return landing !== null && current === landing ? initial : current;
}

export function switchBackPath(): string {
return pickSwitchBackPath(initialUrl, landingUrl, currentUrl());
}
21 changes: 0 additions & 21 deletions src/pinguino/devhub/tests/navigation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import {
classicPath,
interceptOutOfAppLinks,
isDashboardPath,
isInAppPath,
Expand Down Expand Up @@ -92,26 +91,6 @@ describe('isDashboardPath', () => {
});
});

describe('classicPath', () => {
it('translates the add-on feed tab to the classic feed URL', () => {
expect(classicPath('/developers/addon/my-addon/feed', '/developers')).toBe(
'/developers/feed/my-addon',
);
});

it('leaves paths classic has no counterpart for alone', () => {
for (const path of [
'/developers/',
'/developers/addon/my-addon',
'/developers/addon/my-addon/listing',
'/developers/feed/my-addon',
'/en-US/firefox/addon/my-addon',
]) {
expect(classicPath(path, '/developers')).toBe(path);
}
});
});

describe('pageParam', () => {
it('reads a positive page from the query string', () => {
expect(pageParam('?page=3')).toBe(3);
Expand Down
22 changes: 20 additions & 2 deletions src/pinguino/devhub/tests/pages/addon/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,24 @@ async function goto(tag: string, path: string) {
}

describe('addon-routes', () => {
it('renders the add-on page for a slug', async () => {
const root = await goto('addon-routes', 'addon-slug');
it('renders the add-on page for a slug/tab', async () => {
const root = await goto('addon-routes', 'addon-slug/edit');
const page = root.querySelector('devhub-addon');
expect(page).not.toBeNull();
expect(page?.slug).toBe('addon-slug');
expect(page?.tab).toBe('edit');
});

it('redirects a bare slug to its edit tab', async () => {
// The bare :slug route has an `enter` redirect and renders nothing itself.
const root = await goto('addon-routes', 'addon-slug');
expect(root.querySelector('devhub-addon')).toBeNull();
});

it('redirects ownership to the edit authors section', async () => {
// The :slug/ownership route redirects via `enter` and renders nothing.
const root = await goto('addon-routes', 'addon-slug/ownership');
expect(root.querySelector('devhub-addon')).toBeNull();
});

it('carries the tab and the ?page= onto the add-on page', async () => {
Expand All @@ -37,6 +50,11 @@ describe('addon-routes', () => {
const root = await goto('addon-routes', 'submit/agreement');
expect(root.querySelector('submit-routes')).not.toBeNull();
});

it('stubs pages that are not reimplemented yet', async () => {
const root = await goto('addon-routes', 'validate');
expect(root.querySelector('coming-soon')).not.toBeNull();
});
});

describe('submit-routes', () => {
Expand Down
Loading
Loading