Skip to content
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

Add approve/deny buttons and display only approved comments #189

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
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: 1 addition & 1 deletion models/import.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = (event, state) => {
case '/legislation/:shortId/import':
case '/nominations/:shortId/import':
case '/:username/:shortId/import':
if (!state.user || !state.user.is_admin) return [{ ...state, loading: { page: true } }, redirect('/')]
if (!state.user) return [{ ...state, loading: { page: true } }, redirect('/join')]
return [state]
default:
return [state]
Expand Down
176 changes: 176 additions & 0 deletions views/import-author-form-new.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const { APP_NAME } = process.env
const { avatarURL, handleForm, html } = require('../helpers')
const { icon } = require('@fortawesome/fontawesome-svg-core')
const { faUser } = require('@fortawesome/free-solid-svg-icons/faUser')
const { faLink } = require('@fortawesome/free-solid-svg-icons/faLink')
const { faTwitter } = require('@fortawesome/free-brands-svg-icons/faTwitter')
const { faExclamationTriangle } = require('@fortawesome/free-solid-svg-icons/faExclamationTriangle')
const { faEdit } = require('@fortawesome/free-solid-svg-icons/faEdit')
const { faEnvelope } = require('@fortawesome/free-solid-svg-icons/faEnvelope')
const { faPlus } = require('@fortawesome/free-solid-svg-icons/faPlus')

module.exports = (state, dispatch) => {
const { location } = state
const tab = location.query.tab || 'search'
const path = location.path

return html`
<div>
<div class="tabs">
<ul>
<li class=${tab === 'search' ? 'is-active' : ''}><a href=${`${path}?tab=search`}>Search Liquid Profiles</a></li>
<li class=${tab === 'email' ? 'is-active' : ''}><a href=${`${path}?tab=email`}>Add by Email</a></li>
<li class=${tab === 'twitter' ? 'is-active' : ''}><a href=${`${path}?tab=twitter`}>Add by Twitter</a></li>
</ul>
</div>
${tab === 'email' ? addAuthorByEmailForm(state, dispatch) : []}
${tab === 'twitter' ? addAuthorByTwitterForm() : []}
${tab === 'search' ? addAuthorBySearchForm(state, dispatch) : []}
</div>
`
}

const addAuthorByEmailForm = (state) => {
const { error } = state

return html`
<div class="field is-horizontal">
<div class="field-body">
<div class="field">
<div class="control has-icons-left">
<input name="author_name" required class="input" placeholder="First and Last Name" />
<span class="icon is-small is-left">${icon(faUser)}</span>
</div>
</div>
<div class="field">
<div class="control has-icons-left">
<input autocomplete="off" name="profile_image_url" class="input" type="text" placeholder="Profile image link" />
<span class="icon is-small is-left">${icon(faLink)}</span>
</div>
</div>
<div class="field">
<div class="control has-icons-left">
<input autocomplete="off" name="email" class=${`input ${error && error.email ? 'is-danger' : ''}`} type="text" required placeholder="Email" />
${error && error.email
? html`<span class="icon is-small is-left">${icon(faExclamationTriangle)}</span>`
: html`<span class="icon is-small is-left">${icon(faEnvelope)}</span>`
}
${error && error.email ? html`<p class="help is-danger">${error.message}</p>` : ''}
</div>
</div>
</div>
</div>
<p class="is-size-7">They'll be sent a <strong>notification email</strong>.</p>
`
}

const addAuthorByTwitterForm = () => {
return html`
<div class="field">
<div class="control has-icons-left">
<input name="twitter_username" required class="input" placeholder="@username" />
<span class="icon is-small is-left">${icon(faTwitter)}</span>
</div>
</div>
<p class="is-size-7">They'll be sent an <a href="https://twitter.com/liquid_notifs" target="_blank"><strong>invitation tweet</strong></a>.</p>
`
}

const addAuthorBySearchForm = (state, dispatch) => {
const { error, loading, cookies, authorSearchResults = [], authorSearchTerms } = state

return html`
<script>
var autoSubmitAuthorSearchTimeout;
function autoSubmitAuthorSearch(event) {
if (autoSubmitAuthorSearchTimeout) {
clearTimeout(autoSubmitAuthorSearchTimeout);
}
autoSubmitAuthorSearchTimeout = setTimeout(function () {
var el = document.getElementById('search_author_submit');
if (el) el.click();
}, 750);
}
</script>
<form method="POST" onsubmit=${handleForm(dispatch, { type: 'import:authorSearched' })}>
<label for="add_author[search]" class="label has-text-weight-normal">Search for author among public ${APP_NAME} profiles:</label>
<div class="field has-addons">
<div class="${`control is-expanded has-icons-left ${loading.authorSearch ? 'is-loading' : ''}`}">
<input autocomplete="off" onkeypress="autoSubmitAuthorSearch()" name="add_author[search]" class=${`input ${error && error.email ? 'is-danger' : ''}`} type="text" placeholder="Name or @username'}" />
${error && error.message
? html`<span class="icon is-small is-left">${icon(faExclamationTriangle)}</span>`
: html`<span class="icon is-small is-left">${icon(faUser)}</span>`
}
${error && error.message ? html`<p class="help is-danger">${error.message}</p>` : ''}
</div>
<div class="control">
<button id="search_author_submit" type="submit" class="button">
<span>Search</span>
</button>
</div>
</div>
</form>
<br />
<div>
${authorSearchTerms && !authorSearchResults.length ? html`<p>No results for "<strong>${authorSearchTerms}</strong>"</p>` : ''}
${authorSearchResults.map(result => searchResult(cookies, result, dispatch))}
${authorSearchTerms ? html`<br /><p class="notification has-text-grey">Can't find who you're looking for?<br />Add them by <a href="?tab=email">email</a> or <a href="?tab=twitter">Twitter username</a>.</p>` : ''}
</div>
`
}

const searchResult = (cookies, result, dispatch) => {
const { first_name, id, last_name, username, twitter_username } = result

return html`
<div class="media">
<div class="media-left">
<div class="image is-32x32">
${username || twitter_username
? html`
<a href="${username ? `/${username}` : `/twitter/${twitter_username}`}" target="_blank">
<img src=${avatarURL(result)} class="is-rounded" />
</a>
` : html`
<img src=${avatarURL(result)} class="is-rounded" />
`}
</div>
</div>
<div class="media-content">
<a href="${username ? `/${username}` : `/twitter/${twitter_username}`}" target="_blank">
<span>${first_name} ${last_name}</span>
<span class="has-text-grey is-size-7">@${username || twitter_username}</span>
</a>
</div>
<div class="media-right">
${cookies.author_id && cookies.author_id === id
? searchResultAdded({ id }, dispatch) : searchResultAdd(id, username, twitter_username, dispatch)}
</div>
</div>
`
}

const searchResultAdd = (id, username, twitter_username, dispatch) => {
return html`
<form method="POST" onsubmit=${handleForm(dispatch, { type: 'import:addedAuthorViaSearch' })}>
<input name="author_id" type="hidden" value="${id}" />
<input name="author_username" type="hidden" value="${username || twitter_username}" />
<button class="button is-outline is-small" type="submit">
<span class="icon">${icon(faPlus)}</span>
<span>Select author</span>
</button>
</form>
`
}

const searchResultAdded = ({ id }) => {
return html`
<form style="display: inline;" method="POST">
<input name="author_id" type="hidden" value="${id}" />
<button class="button is-small" disabled type="submit">
<span class="icon">${icon(faEdit)}</span>
<span>Current author</span>
</button>
</form>
`
}
24 changes: 12 additions & 12 deletions views/import-vote-page.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
const { handleForm, html } = require('../helpers')

module.exports = ({ error, location, user }, dispatch) => {
module.exports = (state, dispatch) => {
const { error, location, user } = state
return html`
<section class="section">
<div class="container is-widescreen">
<h2 class="title is-size-5">Import Argument to <a href=${location.path.slice(0, -7)}>${location.params.shortId}</a></h2>
<h2 class="title is-size-5">Import External Opinion Related to <a href=${location.path.slice(0, -7)}>${location.params.shortId}</a></h2>

${!user || !user.is_admin ? html`<div class="notification is-danger">You do not have permission to import votes.</div>` : ''}
${!user ? html`<div class="notification is-danger">Login to import comments.</div>` : html`<div class="is-size-5"><p>Once imported & approved, it will be displayed alongside other comments.</p><br /></div>`}

<form onsubmit=${handleForm(dispatch, { type: 'import:voteImportFormSubmitted', short_id: location.params.shortId })} class=${user && user.is_admin ? '' : 'is-hidden'}>
<form onsubmit=${handleForm(dispatch, { type: 'import:voteImportFormSubmitted', short_id: location.params.shortId })} class=${user ? '' : 'is-hidden'}>

${error ? html`<div class="notification is-danger">${error.message}</div>` : ''}

<div class="field">
<label class="label">Twitter Username:</label>
<div class="control">
<input name="twitter_username" required class="input" placeholder="@username" />
</div>
</div>
<div class="field">
<label class="label">Position:</label>
<div class="control">
Expand All @@ -29,12 +35,6 @@ module.exports = ({ error, location, user }, dispatch) => {
</label>
</div>
</div>
<div class="field">
<label class="label">Twitter Username:</label>
<div class="control">
<input name="twitter_username" required class="input" placeholder="@username" />
</div>
</div>
<div class="field">
<label class="label">Source URL:</label>
<div class="control">
Expand All @@ -50,7 +50,7 @@ module.exports = ({ error, location, user }, dispatch) => {
<div class="field">
<label class="label">Comment:</label>
<div class="control">
<textarea required name="comment" autocomplete="off" class="textarea" placeholder="Copy an excerpt from an externally published opinion to add to the bill page.\nOnce imported & approved, anyone will be able to Back it."></textarea>
<textarea required name="comment" autocomplete="off" class="textarea" placeholder="Copy an excerpt from the opinion."></textarea>
</div>
</div>
<div class="field">
Expand Down
28 changes: 17 additions & 11 deletions views/measure-comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ module.exports = (state, dispatch) => {
const comments = (measure.comments || []).map((id) => votes[id])
return html`
<div>
${displayFilters ? filtersView(state, dispatch) : html``}
${displayFilters ? filtersView(state, dispatch) : html`
<div class="control is-right">
<a href=${`${location.path}/import`} class="button is-link has-text-weight-semibold is-small">
<span class="icon">${icon(faPlus)}</span>
<span>Import external argument</span>
</a>
</div>
`}
${loading.comments ? activityIndicator() : html``}
${!loading.comments && comments.length ? comments.map(voteOrSignatureView(state, dispatch)) : html``}
${!loading.comments && !comments.length ? noCommentsView() : html``}
Expand All @@ -28,7 +35,7 @@ const voteOrSignatureView = (state, dispatch) => (vote) => {
const noCommentsView = () => html`<p class="has-text-centered has-text-grey">No comments yet.</p>`

const filtersView = (state, dispatch) => {
const { loading, location, measures, user } = state
const { loading, location, measures } = state
const measure = measures[location.params.shortId]
const pagination = measure.commentsPagination || { offset: 0, limit: 25 }
const { path, query } = location
Expand Down Expand Up @@ -107,16 +114,15 @@ const filtersView = (state, dispatch) => {
</button>
</div>
</div>
${user && user.is_admin ? html`
<div class="field is-narrow">
<div class="control">
<a href=${`${location.path}/import`} class="button is-link has-text-weight-semibold is-small">
<span class="icon">${icon(faPlus)}</span>
<span>Import external argument</span>
</a>
</div>

<div class="field is-narrow">
<div class="control">
<a href=${`${location.path}/import`} class="button is-link has-text-weight-semibold is-small">
<span class="icon">${icon(faPlus)}</span>
<span>Import external argument</span>
</a>
</div>
` : ''}
</div>
</div>
</div>
</form>
Expand Down
46 changes: 32 additions & 14 deletions views/vote.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const { faStar: faStarOutline } = require('@fortawesome/free-regular-svg-icons/f
const { faVoteYea } = require('@fortawesome/free-solid-svg-icons/faVoteYea')
const { faVoteNay } = require('@fortawesome/pro-solid-svg-icons/faVoteNay')
const { faBoxBallot } = require('@fortawesome/pro-solid-svg-icons/faBoxBallot')
const { faPlus } = require('@fortawesome/free-solid-svg-icons/faPlus')
const { faTimes } = require('@fortawesome/free-solid-svg-icons/faTimes')

module.exports = (state, dispatch) => {
const { key, vote, parent, padded = true, displayTitle = false, user, showIcon = false, measures = {} } = state
Expand Down Expand Up @@ -45,21 +47,37 @@ module.exports = (state, dispatch) => {
: html`<img src="${avatarURL}" alt="avatar" class="is-rounded" />`}
</div>
`}
</div>
<div class="media-content">
<div>
<span class="has-text-weight-semibold">
${!is_public && ownVote
? 'You'
: vote.user
? vote.user.public_profile
? html`<a href="${`/${vote.user.username || `twitter/${vote.user.twitter_username}`}`}">${vote.user.first_name} ${vote.user.last_name}</a>`
: html`<span>${vote.user.first_name} ${vote.user.last_name}</span>`
: '[private]'}
</span>
${html`<span>${delegate_name && delegate_rank !== -1 ? 'inherited' : 'voted'} <strong style="${`color: ${position === 'yea' ? 'hsl(141, 80%, 38%)' : (position === 'abstain' ? 'default' : 'hsl(348, 80%, 51%)')};`}">${position}</strong>${delegate_rank !== -1 && delegate_name ? ` vote from ${delegate_name}` : ''}${delegate_rank === -1 && vote_power > 1 && is_public ? html` on behalf of <span class="has-text-weight-semibold">${vote_power}</span> people` : ''}${is_public ? '' : ' privately'}</span>`}
${source_url ? html`<span class="is-size-7"> <a href="${source_url}" target="_blank">[source]</a></span>` : ''}
</div>
<div class="media-content">
<div>
<span class="has-text-weight-semibold">
${!is_public && ownVote
? 'You'
: vote.user
? vote.user.public_profile
? html`<a href="${`/${vote.user.username || `twitter/${vote.user.twitter_username}`}`}">${vote.user.first_name} ${vote.user.last_name}</a>`
: html`<span>${vote.user.first_name} ${vote.user.last_name}</span>`
: '[private]'}
</span>
${html`<span>${delegate_name && delegate_rank !== -1 ? 'inherited' : 'voted'} <strong style="${`color: ${position === 'yea' ? 'hsl(141, 80%, 38%)' : (position === 'abstain' ? 'default' : 'hsl(348, 80%, 51%)')};`}">${position}</strong>${delegate_rank !== -1 && delegate_name ? ` vote from ${delegate_name}` : ''}${delegate_rank === -1 && vote_power > 1 && is_public ? html` on behalf of <span class="has-text-weight-semibold">${vote_power}</span> people` : ''}${is_public ? '' : ' privately'}</span>`}
${source_url ? html`<span class="is-size-7"> <a href="${source_url}" target="_blank">[source]</a></span>` : ''}
</div>
</div>
</div>
${user && user.is_admin ?
html`<br /><div class="buttons has-addons">
<form method="POST" onsubmit=${handleForm(dispatch, { type: 'import:allowImportedComment', measure })}>
<button type="submit" class="button is-small">
<span class="icon is-small">${icon(faPlus)}</span><span>Approve</span>
</button>
</form>
<form method="POST" onsubmit=${handleForm(dispatch, { type: 'import:denyImportedComment', measure })}>
<button type="submit" class="button is-small">
<span class="icon is-small">${icon(faTimes)}</span><span>Deny</span>
</button>
</form>
</div>`
: ``}
${displayTitle ? html`<div><a class="has-text-weight-semibold" href="${measure_url}">${measure_title}</a></div>` : ''}
${commentContent(key, vote, parent, dispatch)}
<div class="${`field is-grouped ${!is_public && !ownVote ? 'is-hidden' : ''}`}">
Expand Down