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
8 changes: 5 additions & 3 deletions app/components/crate-sidebar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,13 @@
{{/unless}}
{{/if}}

<a
href="mailto:[email protected]?subject=The%20%22{{@crate.name}}%22%20crate&body=I'm%20reporting%20the%20https%3A%2F%2Fcrates.io%2Fcrates%2F{{@crate.name}}%20crate%20because%3A%0A%0A-%20%5B%20%5D%20it%20contains%20spam%0A-%20%5B%20%5D%20it%20is%20name-squatting%20(reserving%20a%20crate%20name%20without%20content)%0A-%20%5B%20%5D%20it%20is%20abusive%20or%20otherwise%20harmful%0A-%20%5B%20%5D%20it%20contains%20a%20vulnerability%20(please%20try%20to%20contact%20the%20crate%20author%20first)%0A-%20%5B%20%5D%20it%20is%20violating%20the%20usage%20policy%20in%20some%20other%20way%20(please%20specify%20below)%0A%0AAdditional%20details%3A%0A%0A%3Cplease%20add%20more%20information%20if%20you%20can%3E"
<LinkTo
@route="support"
@query={{hash inquire="crate-violation" [email protected]}}
data-test-id="link-crate-report"
local-class="report-button"
>
Report crate
</a>
</LinkTo>
</div>
</section>
2 changes: 1 addition & 1 deletion app/components/footer.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<h1>Get Help</h1>
<ul role="list">
<li><a href="https://doc.rust-lang.org/cargo/">The Cargo Book</a></li>
<li><a href="mailto:[email protected]">Email Support</a></li>
<li><LinkTo @route="support" @query={{this.pristineSupportQuery}}>Support</LinkTo></li>
<li><a href="https://status.crates.io/">System Status</a></li>
<li><a href="https://github.com/rust-lang/crates.io/issues/new/choose">Report a bug</a></li>
</ul>
Expand Down
11 changes: 11 additions & 0 deletions app/components/footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';

export default class Footer extends Component {
@service pristineQuery;

get pristineSupportQuery() {
let params = this.pristineQuery.paramsFor('support');
return params;
}
}
89 changes: 89 additions & 0 deletions app/components/support/crate-report-form.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<form
local-class="report-form"
data-testid="crate-report-form"
...attributes
{{on "submit" (prevent-default this.submit)}}
>
<h2>Report A Crate</h2>

<fieldset local-class="form-group" data-test-id="fieldset-crate">
{{#let (unique-id) as |id|}}
<label for={{id}} local-class="form-group-name">
Crate
</label>
<Input
id={{id}}
@type="text"
@value={{this.crate}}
autocomplete="off"
aria-required="true"
aria-invalid={{if this.crateInvalid "true" "false"}}
local-class="crate-input"
data-test-id="crate-input"
{{auto-focus}}
{{on "input" this.resetCrateValidation}}
/>
{{#if this.crateInvalid}}
<div local-class="form-group-error" data-test-id="crate-invalid">
Please specify a crate.
</div>
{{/if}}
{{/let}}
</fieldset>

<fieldset local-class="form-group" data-test-id="fieldset-reasons">
<div local-class="form-group-name">Reasons</div>
<ul role="list" local-class="reasons-list {{if this.reasonsInvalid "invalid"}}">
{{#each this.reasons as |option|}}
<li>
<label>
<Input
@type="checkbox"
@checked={{this.isReasonSelected option.reason}}
name={{ option.reason }}
data-test-id="{{ option.reason }}-checkbox"
{{on "change" (fn this.toggleReason option.reason)}}
/>
{{option.description}}
</label>
</li>
{{/each}}
</ul>
{{#if this.reasonsInvalid}}
<div local-class="form-group-error" data-test-id="reasons-invalid">
Please choose reasons to report.
</div>
{{/if}}
</fieldset>

<fieldset local-class="form-group" data-test-id="fieldset-detail">
{{#let (unique-id) as |id|}}
<label for={{id}} local-class="form-group-name">Detail</label>
<Textarea
id={{id}}
@value={{this.detail}}
local-class="detail {{if this.detailInvalid "invalid"}}"
aria-required={{if this.detailInvalid "true" "false" }}
aria-invalid={{if this.detailInvalid "true" "false"}}
rows="5"
data-test-id="detail-input"
{{on "input" this.resetDetailValidation}}
/>
{{#if this.detailInvalid}}
<div local-class="form-group-error" data-test-id="detail-invalid">
Please provide some detail.
</div>
{{/if}}
{{/let}}
</fieldset>

<div local-class="buttons">
<button
type="submit"
local-class="report-button"
data-test-id="report-button"
>
Report
</button>
</div>
</form>
105 changes: 105 additions & 0 deletions app/components/support/crate-report-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import window from 'ember-window-mock';

const REASONS = [
{
reason: 'spam',
description: 'it contains spam',
},
{
reason: 'name-squatting',
description: 'it is name-squatting (reserving a crate name without content)',
},
{
reason: 'abuse',
description: 'it is abusive or otherwise harmful',
},
{
reason: 'security',
description: 'it contains a vulnerability (please try to contact the crate author first)',
},
{
reason: 'other',
description: 'it is violating the usage policy in some other way (please specify below)',
},
];

export default class CrateReportForm extends Component {
@service store;

@tracked crate = '';
@tracked selectedReasons = [];
@tracked detail = '';
@tracked crateInvalid = false;
@tracked reasonsInvalid = false;
@tracked detailInvalid = false;

reasons = REASONS;

constructor() {
super(...arguments);
this.crate = this.args.crate;
}

validate() {
this.crateInvalid = !this.crate || !this.crate.trim();
this.reasonsInvalid = this.selectedReasons.length === 0;
this.detailInvalid = this.selectedReasons.includes('other') && !this.detail?.trim();
return !this.crateInvalid && !this.reasonsInvalid && !this.detailInvalid;
}

@action resetCrateValidation() {
this.crateInvalid = false;
}

@action resetDetailValidation() {
this.detailInvalid = false;
}

@action isReasonSelected(reason) {
return this.selectedReasons.includes(reason);
}

@action toggleReason(reason) {
this.selectedReasons = this.selectedReasons.includes(reason)
? this.selectedReasons.filter(it => it !== reason)
: [...this.selectedReasons, reason];
this.reasonsInvalid = false;
}

@action
submit() {
if (!this.validate()) {
return;
}

let mailto = this.composeMail();
window.open(mailto, '_self');
}

composeMail() {
let crate = this.crate;
let reasons = this.reasons
.map(({ reason, description }) => {
let selected = this.isReasonSelected(reason);
return `${selected ? '- [x]' : '- [ ]'} ${description}`;
})
.join('\n');
let body = `I'm reporting the https://crates.io/crates/${crate} crate because:

${reasons}

Additional details:

${this.detail}
`;
let subject = `The "${crate}" crate`;
let address = '[email protected]';
let mailto = `mailto:${address}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
return mailto;
}
}
76 changes: 76 additions & 0 deletions app/components/support/crate-report-form.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.report-form {
background-color: var(--main-bg);
padding: 0.5rem 1rem;
}

.form-group {
border: none;
margin: 0;
padding: 0;

& + & {
margin-top: 1rem;
}
}

.form-group-name {
composes: form-group-name from '../../styles/settings/tokens/new.module.css';
align-items: center;
}

.crate-input {
composes: name-input from '../../styles/settings/tokens/new.module.css';
}

.reasons-list {
composes: scopes-list from '../../styles/settings/tokens/new.module.css';
label {
flex-wrap: nowrap;
}
input {
align-self: center;
}
}

.detail {
padding: var(--space-2xs);
background-color: light-dark(white, #141413);
border: 1px solid var(--gray-border);
border-radius: var(--space-3xs);
resize: vertical;
width: 100%;

&.invalid {
background: light-dark(#fff2f2, #170808);
border-color: red;
}
}

.form-group-error {
composes: form-group-error from '../../styles/settings/tokens/new.module.css';
}

.buttons {
composes: buttons from '../../styles/settings/tokens/new.module.css';
justify-content: end;
gap: 2rem;
}

.button {
&:focus {
outline: 1px solid var(--bg-color-top-dark);
outline-offset: 2px;
}
}

.report-button {
composes: button;
composes: button small from '../../styles/shared/buttons.module.css';
border-radius: var(--space-3xs);
}

.cancel-button {
composes: button;
composes: tan-button small from '../../styles/shared/buttons.module.css';
border-radius: var(--space-3xs);
}
24 changes: 24 additions & 0 deletions app/controllers/support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';

const SUPPORTS = [
{
inquire: 'crate-violation',
label: 'Report a crate that violates policies',
},
];

const VALID_INQUIRE = new Set(SUPPORTS.map(s => s.inquire));

export default class SupportController extends Controller {
queryParams = ['inquire', 'crate'];

@tracked inquire;
Copy link
Member

Choose a reason for hiding this comment

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

since this is in a controller I think the property keeps its value across page navigations. in other words: if you press the "report crate" button the property will be set, if you navigate away from the page, then you press the support link in the footer, you will not get to the overview state, but to the "report crate" state again. I'm not 100% sure about this, but we should probably add a test to ensure that navigating to /support without query params resets the state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One more thing I noticed is that, if you're on the support page with query, if the support link in footer is defined as <LinkTo @route="support" /> it will still inherit those query params. There doesn't seem to be a simple way to avoid this inheritance in the template. You need to use hash to manual overwrite all query params.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm wondering if there's a way to test this without <LinkTo @route="support"/>. We already have <LinkTo @route="support" @query={{hash inquire=null crate=null }}>Support</LinkTo> that reset the query params by setting their values to null.

Copy link
Member

Choose a reason for hiding this comment

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

not sure about playwright, but in the Ember.js test suite visit('/support') might be sufficient as it does not reload the whole app

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I just commented out resetController and expected the following to fail, but it didn't 😅 .

  test('should not retain query params when exiting and then returning', async function (assert) {
    await visit('/support?inquire=crate-violation');
    assert.strictEqual(currentURL(), '/support?inquire=crate-violation');
    assert
      .dom('[data-test-id="support-main-content"] section')
      .exists({ count: 1 })
      .hasAttribute('data-test-id', 'crate-violation-section');

    await visit('/');
    assert.strictEqual(currentURL(), '/');
    await visit('/support');
    assert.strictEqual(currentURL(), '/support');
    assert
      .dom('[data-test-id="support-main-content"] section')
      .exists({ count: 1 })
      .hasAttribute('data-test-id', 'inquire-list-section');
  });

However, if there's a <LinkTo @route="support">, I can see that the generated URL has sticky query params.

Copy link
Member

Choose a reason for hiding this comment

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

interesting, maybe I was wrong then :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I figured out how to test this in qunit but not playwright. I don't know how to mock the route in playwright QQ.

@tracked crate;

supports = SUPPORTS;

get supported() {
return VALID_INQUIRE.has(this.inquire);
}
}
1 change: 1 addition & 0 deletions app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Router.map(function () {
this.route('data-access');
this.route('confirm', { path: '/confirm/:email_token' });
this.route('accept-invite', { path: '/accept-invite/:token' });
this.route('support');

this.route('catch-all', { path: '*path' });
});
13 changes: 13 additions & 0 deletions app/routes/support.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Route from '@ember/routing/route';

export default class CrateRoute extends Route {
resetController(controller, isExiting) {
super.resetController(...arguments);
// reset queryParams when exiting
if (isExiting) {
for (let param of controller.queryParams) {
controller.set(param, null);
}
}
}
}
9 changes: 9 additions & 0 deletions app/services/pristine-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getOwner } from '@ember/owner';
import Service from '@ember/service';

export default class PristineParamsService extends Service {
paramsFor(route) {
let params = getOwner(this).lookup(`controller:${route}`)?.queryParams || [];
return Object.fromEntries(params.map(k => [k, null]));
}
}
15 changes: 15 additions & 0 deletions app/styles/support.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.inquire-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-s);
list-style: none;
padding: 0;
}

.link {
composes: link from '../components/front-page-list/item.module.css';
justify-content: center;
padding: var(--space-xs) var(--space-s);
height: inherit;
min-height: var(--space-2xl);
}
Loading
Loading