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
1 change: 1 addition & 0 deletions app/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ section.event {

.whoops {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: var(--spacing-6);
Expand Down
116 changes: 56 additions & 60 deletions app/controllers/project-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { action, computed, set } from '@ember/object';
import { inject as service } from '@ember/service';
import { readOnly, alias } from '@ember/object/computed';
import Controller, { inject as controller } from '@ember/controller';
import Controller from '@ember/controller';
import { A } from '@ember/array';
import values from 'lodash.values';
import groupBy from 'lodash.groupby';
Expand All @@ -25,10 +25,6 @@ export default class ProjectVersionController extends Controller {
@service router;
@service('project') projectService;

@controller('project-version.classes.class') classController;
@controller('project-version.modules.module') moduleController;
@controller('project-version.namespaces.namespace') namespaceController;

@alias('filterData.sideNav.showPrivate')
showPrivateClasses;

Expand Down Expand Up @@ -141,61 +137,61 @@ export default class ProjectVersionController extends Controller {

@action
updateProject(project, ver /*, component */) {
let projectVersionID = ver.compactVersion;
let endingRoute;
switch (this.router.currentRouteName) {
case 'project-version.classes.class': {
let className = this._getEncodedNameForCurrentClass();
endingRoute = `classes/${className}`;
break;
}
case 'project-version.modules.module': {
let moduleName = encodeURIComponent(this.moduleController.model.name);
endingRoute = `modules/${moduleName}`;
break;
}
case 'project-version.namespaces.namespace': {
let namespaceName = this.namespaceController.model.name;
endingRoute = `namespaces/${namespaceName}`;
break;
}
default:
endingRoute = '';
break;
}
// if the user is navigating to/from api versions >= 2.16, take them
// to the home page instead of trying to translate the url
let shouldConvertPackages = this._shouldConvertPackages(
ver,
this.projectService.version,
);
let isEmberProject = project === 'ember';

if (!isEmberProject || !shouldConvertPackages) {
this.router.transitionTo(
`/${project}/${projectVersionID}/${endingRoute}`,
);
} else {
this.router.transitionTo(`/${project}/${projectVersionID}`);
}
}

_getEncodedNameForCurrentClass() {
// escape any reserved characters for url, like slashes
return encodeURIComponent(this.classController.model.get('name'));
}

// Input some version info, returns a boolean based on
// whether the user is switching versions for a 2.16 docs release or later.
// The urls for pre-2.16 classes and later packages are quite different
_shouldConvertPackages(targetVer, previousVer) {
let targetVersion = getCompactVersion(targetVer.id);
let previousVersion = getCompactVersion(previousVer);
let previousComparison = semverCompare(previousVersion, '2.16');
let targetComparison = semverCompare(targetVersion, '2.16');
return (
(previousComparison < 0 && targetComparison >= 0) ||
(previousComparison >= 0 && targetComparison < 0)
const currentURL = this.router.currentURL;
this.router.transitionTo(
findEndingRoute({
project,
targetVersion: ver.id,
currentVersion: this.projectService.version,
currentUrlVersion: this.projectService.getUrlVersion(),
currentURL,
currentAnchor: window.location.hash,
}),
);
}
}

export function findEndingRoute({
project,
targetVersion,
currentVersion,
currentUrlVersion,
currentURL,
currentAnchor,
}) {
let projectVersionID = getCompactVersion(targetVersion);
// if the user is navigating to/from api versions Ember >= 2.16 or Ember Data >= 4.0, take them
// to the home page instead of trying to translate the url
if (shouldGoToVersionIndex(project, targetVersion, currentVersion)) {
return `/${project}/${projectVersionID}`;
} else {
return `${currentURL.replace(currentUrlVersion, projectVersionID)}${currentAnchor}`;
}
}

function shouldGoToVersionIndex(project, targetVersion, currentVersion) {
let boundaryVersion;
if (project === 'ember') {
boundaryVersion = '2.16';
} else if (project === 'ember-data') {
boundaryVersion = '4.0';
}
return isCrossingVersionBoundary(
targetVersion,
currentVersion,
boundaryVersion,
);
}

// Input some version info, returns a boolean based on
// whether the user is switching versions for a release or later.
function isCrossingVersionBoundary(targetVer, previousVer, boundaryVersion) {
let targetVersion = getCompactVersion(targetVer);
let previousVersion = getCompactVersion(previousVer);
let previousComparison = semverCompare(previousVersion, boundaryVersion);
let targetComparison = semverCompare(targetVersion, boundaryVersion);
return (
(previousComparison < 0 && targetComparison >= 0) ||
(previousComparison >= 0 && targetComparison < 0)
);
}
20 changes: 19 additions & 1 deletion app/routes/project-version/classes/class.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,26 @@ export default class ClassRoute extends Route {
});
}

redirect(model) {
redirect(model, transition) {
if (model.isError) {
// Transitioning to the same route, probably only changing version
// Could explicitly check by comparing transition.to and transition.from
if (transition.to.name === transition?.from?.name) {
const projectVersionRouteInfo = transition.to.find(function (item) {
return item.params?.project_version;
});
const attemptedVersion =
projectVersionRouteInfo.params?.project_version;
const attemptedProject = projectVersionRouteInfo.params?.project;
let error = new Error(
`We could not find ${transition.to.localName} ${transition.to.params[transition.to.paramNames[0]]} in v${attemptedVersion} of ${attemptedProject}.`,
);
error.status = 404;
error.attemptedProject = attemptedProject;
error.attemptedVersion = attemptedVersion;
throw error;
}

let error = new Error(
'Error retrieving model in routes/project-version/classes/class',
);
Expand Down
24 changes: 19 additions & 5 deletions app/routes/project-version/functions/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default class FunctionRoute extends Route {
return model?.fn?.name;
}

async model(params) {
async model(params, transition) {
const pVParams = this.paramsFor('project-version');
const { project, project_version: compactVersion } = pVParams;

Expand All @@ -38,10 +38,24 @@ export default class FunctionRoute extends Route {
`${project}-${projectVersion}-${className}`.toLowerCase(),
);
} catch (e) {
fnModule = await this.store.find(
'namespace',
`${project}-${projectVersion}-${className}`.toLowerCase(),
);
try {
fnModule = await this.store.find(
'namespace',
`${project}-${projectVersion}-${className}`.toLowerCase(),
);
} catch (e2) {
if (transition.to.name === transition?.from?.name) {
let error = new Error(
`We could not find function ${className}/${functionName} in v${compactVersion} of ${project}.`,
);
error.status = 404;
error.attemptedProject = project;
error.attemptedVersion = compactVersion;
throw error;
} else {
throw e2;
}
}
}

return {
Expand Down
12 changes: 10 additions & 2 deletions app/templates/error.hbs
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
<article class="whoops">
{{#if (eq this.model.status 404)}}
<h2 class="whoops__title">Ack! 404 friend, you're in the wrong place</h2>
<div class="whoops__message">
{{#if this.model.attemptedVersion}}
<h3>
{{this.model.message}}
</h3>
<p>
Modules, classes, and functions sometimes move around or are renamed across versions.
Try the <LinkTo @route="project-version" @models={{array this.model.attemptedProject this.model.attemptedVersion}} data-test-version-index-link>v{{this.model.attemptedVersion}} API docs index.</LinkTo>
</p>
{{else}}
<p>
This page wasn't found. Please try the <LinkTo @route="index">API docs page</LinkTo>.
If you expected something else to be here, please file a <a href="https://github.com/ember-learn/ember-api-docs/issues/new" target="_blank" rel="noopener noreferrer">ticket</a>.
</p>
</div>
{{/if}}
{{else}}
<h2 class="whoops__title">
Whoops! Something went wrong.
Expand Down
92 changes: 92 additions & 0 deletions tests/acceptance/switch-versions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ async function waitForSettled() {
await settled();
}

const versionIndexLinkSelector = '[data-test-version-index-link]';

module('Acceptance | version navigation', function (hooks) {
setupApplicationTest(hooks);

Expand Down Expand Up @@ -38,6 +40,23 @@ module('Acceptance | version navigation', function (hooks) {
);
});

test('switching versions from release', async function (assert) {
await visit('/ember/release/modules/@glimmer%2Ftracking');

assert.equal(
currentURL(),
'/ember/release/modules/@glimmer%2Ftracking',
'navigated to release',
);
await selectChoose('.ember-power-select-trigger', '6.4');

assert.equal(
currentURL(),
'/ember/6.4/modules/@glimmer%2Ftracking',
'navigated to v6.4 class',
);
});

test('switching namespace versions less than 2.16 should retain namespace page', async function (assert) {
await visit('/ember/2.7/namespaces/Ember');
await waitForSettled();
Expand Down Expand Up @@ -165,6 +184,21 @@ module('Acceptance | version navigation', function (hooks) {
);
});

test('switching between versions on a function works', async function (assert) {
await visit('/ember/6.5/functions/@ember%2Fdebug/debug');
assert.strictEqual(
currentURL(),
'/ember/6.5/functions/@ember%2Fdebug/debug',
);

await selectChoose('.ember-power-select-trigger', '6.4');

assert.strictEqual(
currentURL(),
'/ember/6.4/functions/@ember%2Fdebug/debug',
);
});

test('switching versions works if class name includes slashes', async function (assert) {
await visit('/ember/3.4/classes/@ember%2Fobject%2Fcomputed');
assert.equal(
Expand Down Expand Up @@ -279,4 +313,62 @@ module('Acceptance | version navigation', function (hooks) {
'navigated to v1.13 class',
);
});

test('switching to a version that is missing a module offers a link to the API index for that version', async function (assert) {
await visit('/ember/6.4/modules/@glimmer%2Ftracking%2Fprimitives%2Fcache');
assert.strictEqual(
currentURL(),
'/ember/6.4/modules/@glimmer%2Ftracking%2Fprimitives%2Fcache',
);

await selectChoose('.ember-power-select-trigger', '3.10');

assert
.dom()
.includesText(
'We could not find module @glimmer/tracking/primitives/cache in v3.10 of ember.',
);

assert
.dom(versionIndexLinkSelector)
.includesText('v3.10')
.hasAttribute('href', '/ember/3.10');
});

test('switching to a version that is missing a class offers a link to the API index for that version', async function (assert) {
await visit('/ember/3.0/classes/Ember.Debug');
assert.strictEqual(currentURL(), '/ember/3.0/classes/Ember.Debug');

await selectChoose('.ember-power-select-trigger', '4.0');

assert
.dom()
.includesText('We could not find class Ember.Debug in v4.0 of ember.');

assert
.dom(versionIndexLinkSelector)
.includesText('v4.0')
.hasAttribute('href', '/ember/4.0');
});

test('switching to a version that is missing a function offers a link to the API index for that version', async function (assert) {
await visit('/ember/3.28/functions/@glimmer%2Ftracking/tracked');
assert.strictEqual(
currentURL(),
'/ember/3.28/functions/@glimmer%2Ftracking/tracked',
);

await selectChoose('.ember-power-select-trigger', '3.12');

assert
.dom()
.includesText(
'We could not find function @glimmer/tracking/tracked in v3.12 of ember.',
);

assert
.dom(versionIndexLinkSelector)
.includesText('v3.12')
.hasAttribute('href', '/ember/3.12');
});
});
Loading