Skip to content
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
19 changes: 19 additions & 0 deletions app/components/button-with-dialog.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<button
type="button"
...attributes
{{on "click" (fn this.toggleDialog true)}}
>
{{ @text }}
</button>

<div
local-class="dialog {{ if this.isOpen 'open' }}"
role="dialog"
aria-label="dialog"
>
{{#if this.isOpen}}
<div local-class="dialog__content" data-test-dialog-content>
{{yield this.toggleDialog this.isOpen}}
</div>
{{/if}}
</div>
11 changes: 11 additions & 0 deletions app/components/button-with-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class ButtonWithConfirmationDialog extends Component {
@tracked isOpen = false;

@action toggleDialog(state) {
this.isOpen = state === undefined ? !this.isOpen : !!state;
}
}
24 changes: 24 additions & 0 deletions app/components/button-with-dialog.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.dialog {
inset: 0px;
overflow-y: auto;
display: flex;
justify-content: center;
z-index: 20;
transition: all var(--transition-fast) ease-in;
opacity: 0;

&.open {
position: fixed;
opacity: 1;
}
}

.dialog__content {
--shadow: 0 25px 50px light-dark(hsla(51, 50%, 44%, 0.35), #232321);

height: min-content;
border-radius: var(--space-3xs);
box-shadow: var(--shadow);
outline: 1px solid var(--gray-border);
margin-top: 12.5vh;
}
66 changes: 66 additions & 0 deletions app/components/crate-report-form.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<form local-class="report-form" {{on "submit" (prevent-default this.submit)}}>
<h3>Report Crate "{{@crate}}"</h3>

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

<fieldset local-class="form-group" data-test-detail-group>
{{#let (unique-id) as |id|}}
<label for={{id}} local-class="form-group-name">Detail</label>
<textarea
id={{id}}
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-detail
{{on "change" this.updateDetail}}
{{on "input" this.resetDetailValidation}}
/>
{{#if this.detailInvalid}}
<div local-class="form-group-error" data-test-error>
Please provide some detail.
</div>
{{/if}}
{{/let}}
</fieldset>

<div local-class="buttons">
<button
type="button"
local-class="cancel-button"
data-test-cancel
{{on "click" this.cancel}}
>
Cancel
</button>

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

export default class CrateReportForm extends Component {
@tracked selectedReasons = [];
@tracked detail = '';
@tracked reasonsInvalid;
@tracked detailInvalid;

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)',
},
];

constructor() {
super(...arguments);
this.reset();
}

reset() {
this.reasonsInvalid = false;
this.detailInvalid = false;
}

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

@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 updateDetail(event) {
let { value } = event?.target ?? {};
this.detail = value ?? '';
}

@action cancel() {
this.args.close?.();
}

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

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

composeMail() {
let name = this.args.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/${name} crate because:

${reasons}

Additional details:

${this.detail}
`;
let subject = `The "${name}" crate`;
let address = 'help@crates.io';
let mailto = `mailto:${address}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
return mailto;
}
}
69 changes: 69 additions & 0 deletions app/components/crate-report-form.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
.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';
}

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

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

<a
href="mailto:help@crates.io?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"
<ButtonWithDialog
local-class="report-button"
@text="Report crate"
data-test-report-button
as |toggleDialog|
>
Report crate
</a>
<CrateReportForm
@crate={{@crate.name}}
@close={{fn toggleDialog false}}
/>
</ButtonWithDialog>
</div>
</section>
Loading