Skip to content

Inactivate known external links (Re-implementation) #898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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
2 changes: 2 additions & 0 deletions src/components/ContentNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ function renderNode(createElement, references) {
const titlePlainText = node.overridingTitle || reference.title;
return createElement(Reference, {
props: {
identifier: node.identifier,
type: reference.type,
url: reference.url,
kind: reference.kind,
role: reference.role,
Expand Down
44 changes: 42 additions & 2 deletions src/components/ContentNode/Reference.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@
<script>
import { buildUrl } from 'docc-render/utils/url-helper';
import { TopicRole } from 'docc-render/constants/roles';
import AppStore from 'docc-render/stores/AppStore';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this PR urgent? I moved includedArchiveIdentifier to the new IndexStore. When that lands, we will run into merge conflicts, we would probably need to keep includedArchiveIdentifier when we resolve the conflicts and open a new PR to move it to IndexStore. Would you prefer doing that over waiting a bit on this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ignore this comment if we end up getting this metadata from each page's RenderJSON).


import { notFoundRouteName } from 'docc-render/constants/router';
import ReferenceExternalSymbol from './ReferenceExternalSymbol.vue';
import ReferenceExternal from './ReferenceExternal.vue';
import ReferenceInternalSymbol from './ReferenceInternalSymbol.vue';
import ReferenceInternal from './ReferenceInternal.vue';

const TopicReferenceTypes = new Set([
'section',
'topic',
]);

/**
* Link to internal or external resources.
*
Expand Down Expand Up @@ -52,6 +58,9 @@ import ReferenceInternal from './ReferenceInternal.vue';
*/
export default {
name: 'Reference',
data: () => ({
appState: AppStore.state,
}),
computed: {
isInternal({ url }) {
if (!url) {
Expand Down Expand Up @@ -88,11 +97,42 @@ export default {
urlWithParams({ isInternal }) {
return isInternal ? buildUrl(this.url, this.$route.query) : this.url;
},
isActiveComputed({ url, isActive }) {
return !!(url && isActive);
isActiveComputed({
type,
url,
isActive,
isFromIncludedArchive,
}) {
let flag = !!(url && isActive);

if (TopicReferenceTypes.has(type)) {
flag &&= isFromIncludedArchive;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fancy 👀

}

return flag;
},
isFromIncludedArchive({ appState, identifier }) {
const { includedArchiveIdentifiers = [] } = appState;
// for backwards compatibility purposes, treat all references as being
// from included archives if there is no data for it
if (!includedArchiveIdentifiers.length) {
return true;
}

return includedArchiveIdentifiers.some(archiveId => (
identifier?.startsWith(`doc://${archiveId}/`)
));
},
},
props: {
identifier: {
type: String,
required: false,
},
type: {
type: String,
required: false,
},
url: {
type: String,
required: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default {
if (reference && reference.url) {
return createElement(Reference, {
props: {
identifier: this.identifier,
type: reference.type,
url: reference.url,
kind: reference.kind,
role: reference.role,
Expand Down
5 changes: 4 additions & 1 deletion src/components/DocumentationTopic/Relationships.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ export default {
...section,
symbols: section.identifiers.reduce((list, id) => (
this.references[id] ? (
list.concat(this.references[id])
list.concat({
...this.references[id],
identifier: id,
})
) : (
list
)
Expand Down
2 changes: 2 additions & 0 deletions src/components/DocumentationTopic/RelationshipsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
:role="symbol.role"
:kind="symbol.kind"
:url="symbol.url"
:identifier="symbol.identifier"
:type="symbol.type"
>{{symbol.title}}</Reference>
<WordBreak v-else tag="code">{{symbol.title}}</WordBreak>
<ConditionalConstraints
Expand Down
41 changes: 1 addition & 40 deletions src/mixins/referencesProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
*/
import AppStore from 'docc-render/stores/AppStore';

const TopicReferenceTypes = new Set([
'section',
'topic',
]);

export default {
// inject the `store`
inject: {
Expand All @@ -30,40 +25,6 @@ export default {
data: () => ({ appState: AppStore.state }),
computed: {
// exposes the references for the current page
references() {
const {
isFromIncludedArchive,
store: {
state: { references: originalRefs = {} },
},
} = this;
// strip the `url` key from "topic"/"section" refs if their identifier
// comes from an archive that hasn't been included by DocC
return Object.keys(originalRefs).reduce((newRefs, id) => {
const originalRef = originalRefs[id];
const { url, ...refWithoutUrl } = originalRef;
return TopicReferenceTypes.has(originalRef.type) ? ({
...newRefs,
[id]: isFromIncludedArchive(id) ? originalRefs[id] : refWithoutUrl,
}) : ({
...newRefs,
[id]: originalRef,
});
}, {});
},
},
methods: {
isFromIncludedArchive(id) {
const { includedArchiveIdentifiers = [] } = this.appState;
// for backwards compatibility purposes, treat all references as being
// from included archives if there is no data for it
if (!includedArchiveIdentifiers.length) {
return true;
}

return includedArchiveIdentifiers.some(archiveId => (
id?.startsWith(`doc://${archiveId}/`)
));
},
references: ({ store }) => store.state.references,
},
};
80 changes: 80 additions & 0 deletions tests/unit/components/ContentNode/Reference.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,4 +278,84 @@ describe('Reference', () => {
expect(ref.props('url')).toBe('/downloads/foo.zip');
expect(ref.text()).toBe('Foo');
});

it('inactivates refs as appropriate with non-empty `includedArchiveIdentifiers`', () => {
const createWrapper = propsData => shallowMount(Reference, {
localVue,
router,
propsData,
slots: { default: propsData.title },
});
const aa = createWrapper({
identifier: 'doc://A/documentation/A/a',
url: '/documentation/A/a',
title: 'A.A',
type: 'topic',
});
const ab = createWrapper({
identifier: 'doc://A/documentation/A/b',
url: '/documentation/A/b',
title: 'A.B',
type: 'topic',
});
const bb = createWrapper({
identifier: 'doc://B/documentation/B/b',
url: '/documentation/B/b',
title: 'B.B',
type: 'topic',
});
const bbb = createWrapper({
identifier: 'doc://BB/documentation/BB/b',
url: '/documentation/BB/b#b',
title: 'BB.B',
type: 'section',
});
const c = createWrapper({
identifier: 'https://abc.dev',
url: 'https://abc.dev',
title: 'C',
type: 'link',
});

expect(aa.find(ReferenceInternal).props('isActive')).toBe(true);
expect(ab.find(ReferenceInternal).props('isActive')).toBe(true);
expect(bb.find(ReferenceInternal).props('isActive')).toBe(true);
expect(bbb.find(ReferenceInternal).props('isActive')).toBe(true);
expect(c.find(ReferenceExternal).props('isActive')).toBe(true);

const allIncluded = {
appState: {
includedArchiveIdentifiers: ['A', 'B', 'BB'],
},
};
aa.setData(allIncluded);
expect(aa.find(ReferenceInternal).props('isActive')).toBe(true);
ab.setData(allIncluded);
expect(ab.find(ReferenceInternal).props('isActive')).toBe(true);
bb.setData(allIncluded);
expect(bb.find(ReferenceInternal).props('isActive')).toBe(true);
bbb.setData(allIncluded);
expect(bbb.find(ReferenceInternal).props('isActive')).toBe(true);
c.setData(allIncluded);
expect(c.find(ReferenceExternal).props('isActive')).toBe(true);

// with only the B archive included, only internal links within B should
// be active (only bb in this example)
// c is an external link so it will also remain active
const onlyB = {
appState: {
includedArchiveIdentifiers: ['B'],
},
};
aa.setData(onlyB);
expect(aa.find(ReferenceInternal).props('isActive')).toBe(false);
ab.setData(onlyB);
expect(ab.find(ReferenceInternal).props('isActive')).toBe(false);
bb.setData(onlyB);
expect(bb.find(ReferenceInternal).props('isActive')).toBe(true);
bbb.setData(onlyB);
expect(bbb.find(ReferenceInternal).props('isActive')).toBe(false);
c.setData(onlyB);
expect(c.find(ReferenceExternal).props('isActive')).toBe(true);
});
});
39 changes: 0 additions & 39 deletions tests/unit/mixins/referencesProvider.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,43 +91,4 @@ describe('referencesProvider', () => {
expect(inner.exists()).toBe(true);
expect(inner.props('references')).toEqual(references);
});

it('removes `url` data for refs with non-empty `includedArchiveIdentifiers` app state', () => {
// empty `includedArchiveIdentifiers` — no changes to refs
const outer = createOuter();
let inner = outer.find(FakeComponentInner);
expect(inner.exists()).toBe(true);
expect(inner.props('references')).toEqual(references);

// `includedArchiveIdentifiers` contains all refs - no changes to refs
outer.setData({
appState: {
includedArchiveIdentifiers: ['A', 'B', 'BB'],
},
});
inner = outer.find(FakeComponentInner);
expect(inner.exists()).toBe(true);
expect(inner.props('references')).toEqual(references);

// `includedArchiveIdentifiers` only contains archive B — remove `url` field
// from all non-B refs
outer.setData({
appState: {
includedArchiveIdentifiers: ['B'],
},
});
inner = outer.find(FakeComponentInner);
expect(inner.exists()).toBe(true);
const refs3 = inner.props('references');
expect(refs3).not.toEqual(references);
expect(refs3[aa.identifier].title).toBe(aa.title);
expect(refs3[aa.identifier].url).toBeFalsy(); // aa `url` is gone now
expect(refs3[ab.identifier].title).toBe(ab.title);
expect(refs3[ab.identifier].url).toBeFalsy(); // ab `url` is gone now
expect(refs3[bb.identifier].title).toBe(bb.title);
expect(refs3[bb.identifier].url).toBe(bb.url); // bb still has `url`
expect(refs3[bbb.identifier].title).toBe(bbb.title);
expect(refs3[bbb.identifier].url).toBeFalsy(); // bbb `url` is gone now
expect(refs3[c.identifier].url).toBe(c.url); // external link untouched
});
});