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
feat: Add a link to the stats dashboard for add-on owners #3203
Conversation
29d0606
to
e3f014c
Compare
Codecov Report
@@ Coverage Diff @@
## master mozilla/addons-frontend#3203 +/- ##
==========================================
+ Coverage 95.41% 95.43% +0.01%
==========================================
Files 160 160
Lines 3013 3021 +8
Branches 594 595 +1
==========================================
+ Hits 2875 2883 +8
Misses 119 119
Partials 19 19
Continue to review full report at Codecov.
|
Failing test is mozilla/addons#10799 again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Adding createInternalAddon()
is my only major change request -- the rest is cleanup.
@@ -1,22 +1,28 @@ | |||
import React from 'react'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs /* @flow */
if you're going to use it for prop types.
src/amo/components/AddonMeta.js
Outdated
@@ -66,6 +79,13 @@ export class AddonMetaBase extends React.Component { | |||
} | |||
} | |||
|
|||
export const mapStateToProps = (state) => { | |||
return { | |||
userId: state.user ? state.user.id : null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to the beauty of Redux, state.user
will never be falsy. You can just do userId: state.user.id
-- id
will be null when the user is not signed in.
@@ -164,6 +180,13 @@ export class AddonMoreInfoBase extends React.Component { | |||
} | |||
} | |||
|
|||
export const mapStateToProps = (state) => { | |||
return { | |||
userId: state.user ? state.user.id : null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing here
|
||
describe('<AddonMeta>', () => { | ||
function render({ | ||
addon = fakeAddon, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should get into the habit of doing addon = createInternalAddon(fakeAddon)
because the internal addon object is what this component will receive. You can import it from https://github.com/mozilla/addons-frontend/blob/master/src/core/reducers/addons.js#L83
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should fakeAddon
just get wrapped in createInternalAddon()
? I noticed you doing that in another test file a lot and it seems like boilerplate. Maybe we want a helper like fakeAddon({ ...extraProps })
instead of the object we manipulate now.
@@ -53,6 +65,31 @@ describe('<AddonMeta>', () => { | |||
}); | |||
expect(getUserCount(root)).toMatch(/^1\.000/); | |||
}); | |||
|
|||
it('links to stats if add-on author is viewing the page', () => { | |||
const addon = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should also be const addon = createInternalAddon({ ...fakeAddon, ... })
@@ -182,4 +187,29 @@ describe(__filename, () => { | |||
expect(root.find('.AddonMoreInfo-database-id-content')) | |||
.toHaveText('9001'); | |||
}); | |||
|
|||
it('links to stats if add-on author is viewing the page', () => { | |||
const addon = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be addon = createInternalAddon({ ...fakeAddon, slug: ... })
.
Also, while you're in there could you fix the render()
function?
function render(props) {
return shallowUntilTarget(
<AddonMoreInfo
addon={createInternalAddon(fakeAddon)}
i18n={getFakeI18nInst()}
store={store}
{...props}
/>,
AddonMoreInfoBase
);
}
slug: 'coolio', | ||
authors: [ | ||
{ | ||
id: 11, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment here about extending from fakeAddon.authors[0]
}; | ||
const root = render({ | ||
addon, | ||
store: dispatchSignInActions({ userId: 11 }).store, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment here about referencing a variable for userId
.
tests/unit/core/test_utils.js
Outdated
}); | ||
|
||
it('returns false when addon is not set', () => { | ||
expect(isAddonAuthor({ addon: null, userId: 5838591 })).toEqual(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the code relies on falsyness (not just null) then I think you need another test for the addon: undefined
case.
Ideally the function should throw if addon
or userId
is undefined (since it could have been a calling mistake) but I think React might actually set addon
to undefined in some cases because JavaScript is awful.
}); | ||
|
||
it('returns false if add-on has no authors', () => { | ||
const partialAddon = { ...addon, authors: [] }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see authors: null
covered which is in the if branch. However, I don't know if that's very realistic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will work, so I added a test case. I'm not sure either but good to test it to make sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kumar303 reviewed this PR in the meantime, so I only leave comments :)
type PropTypes = { | ||
addon: AddonType | null, | ||
i18n: Object, | ||
userId: number | null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure we have discussed this yet (at least not with me), but Flow users tend to use ?number
to mark a nullable/maybe value: https://flow.org/en/docs/types/maybe/.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I would go for strict typing if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it's up to the design of the component. If it were userId?
then undefined
would be allowed. I think allowing undefined
is risky and we should avoid it whenever possible. I like using null
values to indicate that the property value is still loading.
addon: PropTypes.object.isRequired, | ||
i18n: PropTypes.object.isRequired, | ||
} | ||
props: PropTypes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
@@ -49,7 +55,14 @@ export class AddonMetaBase extends React.Component { | |||
<div className="AddonMeta-item AddonMeta-users"> | |||
<h3 className="visually-hidden">{i18n.gettext('Used by')}</h3> | |||
<p className="AddonMeta-text AddonMeta-user-count"> | |||
{userCount} | |||
{addon && isAddonAuthor({ addon, userId }) ? ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is probably no need for addon &&
because it is checked in the isAddonAuthor()
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, true!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Annoyingly, flow can't understand I won't access that property unless isAddonAuthor
is true
(which it never would be if addon
were null
), so I left this in.
...customProps, | ||
}; | ||
return shallow(<AddonMetaBase {...props} />); | ||
} | ||
|
||
describe('<AddonMeta>', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could update the describe name to use __filename
if you wish.
import Link from 'amo/components/Link'; | ||
import { | ||
dispatchSignInActions, | ||
dispatchClientMetadata, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be imported first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIIIIIICE catch 👍 🤣
@@ -53,6 +65,31 @@ describe('<AddonMeta>', () => { | |||
}); | |||
expect(getUserCount(root)).toMatch(/^1\.000/); | |||
}); | |||
|
|||
it('links to stats if add-on author is viewing the page', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have the opposite test case somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, we didn't. I added one.
import Link from 'amo/components/Link'; | ||
import { | ||
dispatchSignInActions, | ||
dispatchClientMetadata, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be imported first.
it('links to stats if add-on author is viewing the page', () => { | ||
const addon = { | ||
...fakeAddon, | ||
slug: 'coolio', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥇
tests/unit/core/test_utils.js
Outdated
expect(isAddonAuthor({ addon, userId: 5838591 })).toEqual(true); | ||
}); | ||
|
||
it('returns true when userId is not in add-on author object', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it returns false
tests/unit/core/test_utils.js
Outdated
expect(isAddonAuthor({ addon, userId: 5838592 })).toEqual(false); | ||
}); | ||
|
||
it('returns false when addon is not set', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when addon is null
"not set" could be tested with addon: undefined
no?
e3f014c
to
9247b29
Compare
Thanks for the reviews, both. Ready for another r? |
70fdffd
to
ab82c33
Compare
This is all rebased and ready for a final r? before the tag if possible 😄 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The design of renderDefinitions()
is that it's a dumb method. You made it a little too smart :) You pass it definitions to render and that's it. This design allows the loading state to work correctly.
I think it will be a little confusing if we land your changes but I rewrote it for you with more dumb. Let me know what you think. This patch could also be applied as a follow-up on master, your call.
r+ with this patch.
diff --git a/src/amo/components/AddonMoreInfo/index.js b/src/amo/components/AddonMoreInfo/index.js
index d2a47292..0e254c21 100644
--- a/src/amo/components/AddonMoreInfo/index.js
+++ b/src/amo/components/AddonMoreInfo/index.js
@@ -24,7 +24,7 @@ export class AddonMoreInfoBase extends React.Component {
props: PropTypes;
listContent() {
- const { addon, i18n } = this.props;
+ const { addon, i18n, userId } = this.props;
if (!addon) {
return this.renderDefinitions({
@@ -109,6 +109,14 @@ export class AddonMoreInfoBase extends React.Component {
{i18n.gettext('See all beta versions')}
</a>
) : null,
+ statsLink: addon && isAddonAuthor({ addon, userId }) ? (
+ <a
+ className="AddonMoreInfo-stats-link"
+ href={`/addon/${addon.slug}/statistics/`}
+ >
+ {i18n.gettext('Visit stats dashboard')}
+ </a>
+ ) : null,
});
}
@@ -123,8 +131,9 @@ export class AddonMoreInfoBase extends React.Component {
versionHistoryLink,
betaVersionsLink = null,
addonId,
+ statsLink = null,
}: Object) {
- const { addon, i18n, userId } = this.props;
+ const { i18n } = this.props;
return (
<dl className="AddonMoreInfo-contents">
{homepage || supportUrl ? (
@@ -176,18 +185,12 @@ export class AddonMoreInfoBase extends React.Component {
</dt>
) : null}
{betaVersionsLink ? <dd>{betaVersionsLink}</dd> : null}
- {addon && isAddonAuthor({ addon, userId }) ? (
+ {statsLink ? (
<dt className="AddonMoreInfo-stats-title">
{i18n.gettext('Usage Statistics')}
</dt>
) : null}
- {addon && isAddonAuthor({ addon, userId }) ? (
- <dd className="AddonMoreInfo-stats-content">
- <Link href={`/addon/${addon.slug}/statistics/`}>
- {i18n.gettext('Visit stats dashboard')}
- </Link>
- </dd>
- ) : null}
+ {statsLink ? <dd>{statsLink}</dd> : null}
<dt
className="AddonMoreInfo-database-id-title"
title={i18n.gettext(`This ID is useful for debugging and
diff --git a/tests/unit/amo/components/TestAddonMoreInfo.js b/tests/unit/amo/components/TestAddonMoreInfo.js
index 7ada6cd1..10be6da7 100644
--- a/tests/unit/amo/components/TestAddonMoreInfo.js
+++ b/tests/unit/amo/components/TestAddonMoreInfo.js
@@ -4,7 +4,6 @@ import React from 'react';
import AddonMoreInfo, {
AddonMoreInfoBase,
} from 'amo/components/AddonMoreInfo';
-import Link from 'amo/components/Link';
import { createInternalAddon } from 'core/reducers/addons';
import {
dispatchClientMetadata,
@@ -224,7 +223,7 @@ describe(__filename, () => {
store: dispatchSignInActions({ userId: 5 }).store,
});
- const statsLink = root.find('.AddonMoreInfo-stats-content').find(Link);
+ const statsLink = root.find('.AddonMoreInfo-stats-link');
expect(statsLink).toHaveLength(0);
});
@@ -249,7 +248,7 @@ describe(__filename, () => {
store: dispatchSignInActions({ userId: authorUserId }).store,
});
- const statsLink = root.find('.AddonMoreInfo-stats-content').find(Link);
+ const statsLink = root.find('.AddonMoreInfo-stats-link');
expect(statsLink).toHaveLength(1);
expect(statsLink).toHaveProp('children', 'Visit stats dashboard');
expect(statsLink).toHaveProp('href', '/addon/coolio/statistics/');
Oh, also, in my patch I changed |
ab82c33
to
b1fe89b
Compare
b1fe89b
to
7c13be3
Compare
Fixes mozilla/addons#10558.
Links to the stats dashboard from the user count on the
AddonMeta
section and also adds a link in the dashboard.In order to see this locally you'll be signed in and visit an add-on you own.
Screenshots