Skip to content

Commit 3c970a1

Browse files
authored
Merge pull request #9529 from eth3lbert/support-page
Add a support page
2 parents 9c20f1b + ba869e5 commit 3c970a1

File tree

17 files changed

+1175
-4
lines changed

17 files changed

+1175
-4
lines changed

app/components/crate-sidebar.hbs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@
130130
{{/unless}}
131131
{{/if}}
132132

133-
<a
134-
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"
133+
<LinkTo
134+
@route="support"
135+
@query={{hash inquire="crate-violation" crate=@crate.name}}
136+
data-test-id="link-crate-report"
135137
local-class="report-button"
136138
>
137139
Report crate
138-
</a>
140+
</LinkTo>
139141
</div>
140142
</section>

app/components/footer.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<h1>Get Help</h1>
1414
<ul role="list">
1515
<li><a href="https://doc.rust-lang.org/cargo/">The Cargo Book</a></li>
16-
<li><a href="mailto:[email protected]">Email Support</a></li>
16+
<li><LinkTo @route="support" @query={{this.pristineSupportQuery}}>Support</LinkTo></li>
1717
<li><a href="https://status.crates.io/">System Status</a></li>
1818
<li><a href="https://github.com/rust-lang/crates.io/issues/new/choose">Report a bug</a></li>
1919
</ul>

app/components/footer.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { inject as service } from '@ember/service';
2+
import Component from '@glimmer/component';
3+
4+
export default class Footer extends Component {
5+
@service pristineQuery;
6+
7+
get pristineSupportQuery() {
8+
let params = this.pristineQuery.paramsFor('support');
9+
return params;
10+
}
11+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<form
2+
local-class="report-form"
3+
data-testid="crate-report-form"
4+
...attributes
5+
{{on "submit" (prevent-default this.submit)}}
6+
>
7+
<h2>Report A Crate</h2>
8+
9+
<fieldset local-class="form-group" data-test-id="fieldset-crate">
10+
{{#let (unique-id) as |id|}}
11+
<label for={{id}} local-class="form-group-name">
12+
Crate
13+
</label>
14+
<Input
15+
id={{id}}
16+
@type="text"
17+
@value={{this.crate}}
18+
autocomplete="off"
19+
aria-required="true"
20+
aria-invalid={{if this.crateInvalid "true" "false"}}
21+
local-class="crate-input"
22+
data-test-id="crate-input"
23+
{{auto-focus}}
24+
{{on "input" this.resetCrateValidation}}
25+
/>
26+
{{#if this.crateInvalid}}
27+
<div local-class="form-group-error" data-test-id="crate-invalid">
28+
Please specify a crate.
29+
</div>
30+
{{/if}}
31+
{{/let}}
32+
</fieldset>
33+
34+
<fieldset local-class="form-group" data-test-id="fieldset-reasons">
35+
<div local-class="form-group-name">Reasons</div>
36+
<ul role="list" local-class="reasons-list {{if this.reasonsInvalid "invalid"}}">
37+
{{#each this.reasons as |option|}}
38+
<li>
39+
<label>
40+
<Input
41+
@type="checkbox"
42+
@checked={{this.isReasonSelected option.reason}}
43+
name={{ option.reason }}
44+
data-test-id="{{ option.reason }}-checkbox"
45+
{{on "change" (fn this.toggleReason option.reason)}}
46+
/>
47+
{{option.description}}
48+
</label>
49+
</li>
50+
{{/each}}
51+
</ul>
52+
{{#if this.reasonsInvalid}}
53+
<div local-class="form-group-error" data-test-id="reasons-invalid">
54+
Please choose reasons to report.
55+
</div>
56+
{{/if}}
57+
</fieldset>
58+
59+
<fieldset local-class="form-group" data-test-id="fieldset-detail">
60+
{{#let (unique-id) as |id|}}
61+
<label for={{id}} local-class="form-group-name">Detail</label>
62+
<Textarea
63+
id={{id}}
64+
@value={{this.detail}}
65+
local-class="detail {{if this.detailInvalid "invalid"}}"
66+
aria-required={{if this.detailInvalid "true" "false" }}
67+
aria-invalid={{if this.detailInvalid "true" "false"}}
68+
rows="5"
69+
data-test-id="detail-input"
70+
{{on "input" this.resetDetailValidation}}
71+
/>
72+
{{#if this.detailInvalid}}
73+
<div local-class="form-group-error" data-test-id="detail-invalid">
74+
Please provide some detail.
75+
</div>
76+
{{/if}}
77+
{{/let}}
78+
</fieldset>
79+
80+
<div local-class="buttons">
81+
<button
82+
type="submit"
83+
local-class="report-button"
84+
data-test-id="report-button"
85+
>
86+
Report
87+
</button>
88+
</div>
89+
</form>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { action } from '@ember/object';
2+
import { inject as service } from '@ember/service';
3+
import Component from '@glimmer/component';
4+
import { tracked } from '@glimmer/tracking';
5+
6+
import window from 'ember-window-mock';
7+
8+
const REASONS = [
9+
{
10+
reason: 'spam',
11+
description: 'it contains spam',
12+
},
13+
{
14+
reason: 'name-squatting',
15+
description: 'it is name-squatting (reserving a crate name without content)',
16+
},
17+
{
18+
reason: 'abuse',
19+
description: 'it is abusive or otherwise harmful',
20+
},
21+
{
22+
reason: 'security',
23+
description: 'it contains a vulnerability (please try to contact the crate author first)',
24+
},
25+
{
26+
reason: 'other',
27+
description: 'it is violating the usage policy in some other way (please specify below)',
28+
},
29+
];
30+
31+
export default class CrateReportForm extends Component {
32+
@service store;
33+
34+
@tracked crate = '';
35+
@tracked selectedReasons = [];
36+
@tracked detail = '';
37+
@tracked crateInvalid = false;
38+
@tracked reasonsInvalid = false;
39+
@tracked detailInvalid = false;
40+
41+
reasons = REASONS;
42+
43+
constructor() {
44+
super(...arguments);
45+
this.crate = this.args.crate;
46+
}
47+
48+
validate() {
49+
this.crateInvalid = !this.crate || !this.crate.trim();
50+
this.reasonsInvalid = this.selectedReasons.length === 0;
51+
this.detailInvalid = this.selectedReasons.includes('other') && !this.detail?.trim();
52+
return !this.crateInvalid && !this.reasonsInvalid && !this.detailInvalid;
53+
}
54+
55+
@action resetCrateValidation() {
56+
this.crateInvalid = false;
57+
}
58+
59+
@action resetDetailValidation() {
60+
this.detailInvalid = false;
61+
}
62+
63+
@action isReasonSelected(reason) {
64+
return this.selectedReasons.includes(reason);
65+
}
66+
67+
@action toggleReason(reason) {
68+
this.selectedReasons = this.selectedReasons.includes(reason)
69+
? this.selectedReasons.filter(it => it !== reason)
70+
: [...this.selectedReasons, reason];
71+
this.reasonsInvalid = false;
72+
}
73+
74+
@action
75+
submit() {
76+
if (!this.validate()) {
77+
return;
78+
}
79+
80+
let mailto = this.composeMail();
81+
window.open(mailto, '_self');
82+
}
83+
84+
composeMail() {
85+
let crate = this.crate;
86+
let reasons = this.reasons
87+
.map(({ reason, description }) => {
88+
let selected = this.isReasonSelected(reason);
89+
return `${selected ? '- [x]' : '- [ ]'} ${description}`;
90+
})
91+
.join('\n');
92+
let body = `I'm reporting the https://crates.io/crates/${crate} crate because:
93+
94+
${reasons}
95+
96+
Additional details:
97+
98+
${this.detail}
99+
`;
100+
let subject = `The "${crate}" crate`;
101+
let address = '[email protected]';
102+
let mailto = `mailto:${address}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
103+
return mailto;
104+
}
105+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
.report-form {
2+
background-color: var(--main-bg);
3+
padding: 0.5rem 1rem;
4+
}
5+
6+
.form-group {
7+
border: none;
8+
margin: 0;
9+
padding: 0;
10+
11+
& + & {
12+
margin-top: 1rem;
13+
}
14+
}
15+
16+
.form-group-name {
17+
composes: form-group-name from '../../styles/settings/tokens/new.module.css';
18+
align-items: center;
19+
}
20+
21+
.crate-input {
22+
composes: name-input from '../../styles/settings/tokens/new.module.css';
23+
}
24+
25+
.reasons-list {
26+
composes: scopes-list from '../../styles/settings/tokens/new.module.css';
27+
label {
28+
flex-wrap: nowrap;
29+
}
30+
input {
31+
align-self: center;
32+
}
33+
}
34+
35+
.detail {
36+
padding: var(--space-2xs);
37+
background-color: light-dark(white, #141413);
38+
border: 1px solid var(--gray-border);
39+
border-radius: var(--space-3xs);
40+
resize: vertical;
41+
width: 100%;
42+
43+
&.invalid {
44+
background: light-dark(#fff2f2, #170808);
45+
border-color: red;
46+
}
47+
}
48+
49+
.form-group-error {
50+
composes: form-group-error from '../../styles/settings/tokens/new.module.css';
51+
}
52+
53+
.buttons {
54+
composes: buttons from '../../styles/settings/tokens/new.module.css';
55+
justify-content: end;
56+
gap: 2rem;
57+
}
58+
59+
.button {
60+
&:focus {
61+
outline: 1px solid var(--bg-color-top-dark);
62+
outline-offset: 2px;
63+
}
64+
}
65+
66+
.report-button {
67+
composes: button;
68+
composes: button small from '../../styles/shared/buttons.module.css';
69+
border-radius: var(--space-3xs);
70+
}
71+
72+
.cancel-button {
73+
composes: button;
74+
composes: tan-button small from '../../styles/shared/buttons.module.css';
75+
border-radius: var(--space-3xs);
76+
}

app/controllers/support.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Controller from '@ember/controller';
2+
import { tracked } from '@glimmer/tracking';
3+
4+
const SUPPORTS = [
5+
{
6+
inquire: 'crate-violation',
7+
label: 'Report a crate that violates policies',
8+
},
9+
];
10+
11+
const VALID_INQUIRE = new Set(SUPPORTS.map(s => s.inquire));
12+
13+
export default class SupportController extends Controller {
14+
queryParams = ['inquire', 'crate'];
15+
16+
@tracked inquire;
17+
@tracked crate;
18+
19+
supports = SUPPORTS;
20+
21+
get supported() {
22+
return VALID_INQUIRE.has(this.inquire);
23+
}
24+
}

app/router.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Router.map(function () {
6060
this.route('data-access');
6161
this.route('confirm', { path: '/confirm/:email_token' });
6262
this.route('accept-invite', { path: '/accept-invite/:token' });
63+
this.route('support');
6364

6465
this.route('catch-all', { path: '*path' });
6566
});

app/routes/support.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Route from '@ember/routing/route';
2+
3+
export default class CrateRoute extends Route {
4+
resetController(controller, isExiting) {
5+
super.resetController(...arguments);
6+
// reset queryParams when exiting
7+
if (isExiting) {
8+
for (let param of controller.queryParams) {
9+
controller.set(param, null);
10+
}
11+
}
12+
}
13+
}

app/services/pristine-query.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { getOwner } from '@ember/owner';
2+
import Service from '@ember/service';
3+
4+
export default class PristineParamsService extends Service {
5+
paramsFor(route) {
6+
let params = getOwner(this).lookup(`controller:${route}`)?.queryParams || [];
7+
return Object.fromEntries(params.map(k => [k, null]));
8+
}
9+
}

0 commit comments

Comments
 (0)