Skip to content

Commit

Permalink
feat: add poll option image
Browse files Browse the repository at this point in the history
  • Loading branch information
novacuum committed Mar 7, 2024
1 parent 96cc298 commit bcd2e69
Show file tree
Hide file tree
Showing 18 changed files with 323 additions and 54 deletions.
5 changes: 4 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@
->patch('/fof/polls/{id}/votes', 'fof.polls.votes', Controllers\MultipleVotesPollController::class)
->post('/fof/polls/pollImage', 'fof.polls.upload-image', Controllers\UploadPollImageController::class)
->post('/fof/polls/pollImage/{pollId}', 'fof.polls.upload-image-poll', Controllers\UploadPollImageController::class)
->delete('/fof/polls/pollImage/{pollId}', 'fof.polls.delete-image-poll', Controllers\DeletePollImageController::class),
->delete('/fof/polls/pollImage/{pollId}', 'fof.polls.delete-image-poll', Controllers\DeletePollImageController::class)
->post('/fof/polls/pollOptionImage/{pollId}', 'fof.polls.upload-option-image', Controllers\UploadPollOptionImageController::class)
->post('/fof/polls/pollOptionImage/{pollId}/{optionId}', 'fof.polls.upload-option-image-edit', Controllers\UploadPollOptionImageController::class)
->delete('/fof/polls/pollOptionImage/{optionId}', 'fof.polls.delete-option-image', Controllers\DeletePollOptionImageController::class),

(new Extend\Model(Post::class))
->hasMany('polls', Poll::class, 'post_id', 'id'),
Expand Down
97 changes: 67 additions & 30 deletions js/src/forum/components/PollForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import PollControls from '../utils/PollControls';
import PollModel from '../models/Poll';
import PollOption from '../models/PollOption';
import UploadPollImageButton from './UploadPollImageButton';
import UploadPollOptionImageButton from './UploadPollOptionImageButton';
import Poll from '../models/Poll';

interface PollFormAttrs extends ComponentAttrs {
poll: PollModel;
Expand All @@ -21,10 +23,11 @@ interface PollFormAttrs extends ComponentAttrs {
export default class PollForm extends Component<PollFormAttrs, PollFormState> {
protected options: PollOption[] = [];
protected optionAnswers: Stream<string>[] = [];
protected optionImageUrls: Stream<string>[] = [];
protected optionImage: Stream<string>[] = [];
protected optionImageAlt: Stream<string>[] = [];
protected question: Stream<string>;
protected subtitle: Stream<string>;
protected pollImage: Stream<string | null>;
protected image: Stream<string | null>;
protected imageAlt: Stream<string | null>;
protected endDate: Stream<string | null>;
protected publicPoll: Stream<boolean>;
Expand All @@ -43,11 +46,12 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {

this.options = (poll.tempOptions ?? poll.options()) as PollOption[];
this.optionAnswers = this.options.map((o) => Stream(o.answer()));
this.optionImageUrls = this.options.map((o) => Stream(o.imageUrl()));
this.optionImage = this.options.map((o) => Stream(o.image()));
this.optionImageAlt = this.options.map((o) => Stream(o.imageAlt()));

this.question = Stream(poll.question());
this.subtitle = Stream(poll.subtitle());
this.pollImage = Stream(poll.imageUrl());
this.image = Stream(poll.image());
this.imageAlt = Stream(poll.imageAlt());
this.endDate = Stream(this.formatDate(poll.endDate()));
this.publicPoll = Stream(poll.publicPoll());
Expand Down Expand Up @@ -103,12 +107,12 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
<label className="label">{app.translator.trans('fof-polls.forum.modal.poll_image.label')}</label>
<p className="helpText">{app.translator.trans('fof-polls.forum.modal.poll_image.help')}</p>
<UploadPollImageButton name="pollImage" poll={this.state.poll} onUpload={this.pollImageUploadSuccess.bind(this)} />
<input type="hidden" name="pollImage" value={this.pollImage()} />
<input type="hidden" name="pollImage" value={this.image()} />
</div>,
90
);

if (this.pollImage()) {
if (this.image()) {
items.add(
'poll_image_alt',
<div className="Form-group">
Expand Down Expand Up @@ -260,26 +264,9 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
}

displayOptions() {
return Object.keys(this.options).map((option, i) => (
return this.options.map((option, i) => (
<div className="Form-group">
<fieldset className="Poll-answer-input">
<input
className="FormControl"
type="text"
name={'answer' + (i + 1)}
bidi={this.optionAnswers[i]}
placeholder={app.translator.trans('fof-polls.forum.modal.option_placeholder') + ' #' + (i + 1)}
/>
{app.forum.attribute('allowPollOptionImage') ? (
<input
className="FormControl"
type="text"
name={'answerImage' + (i + 1)}
bidi={this.optionImageUrls[i]}
placeholder={app.translator.trans('fof-polls.forum.modal.image_option_placeholder') + ' #' + (i + 1)}
/>
) : null}
</fieldset>
<fieldset className="Poll-answer-input">{this.createOptionFields(option, i).toArray()}</fieldset>
{i >= 2
? Button.component({
type: 'button',
Expand All @@ -292,13 +279,61 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
));
}

createOptionFields(option: PollOption, i: number): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
const number = i + 1;

items.add(
'answer',
<input
className="FormControl"
type="text"
name={'answer' + (i + 1)}
bidi={this.optionAnswers[i]}
placeholder={app.translator.trans('fof-polls.forum.modal.option_placeholder') + ' #' + number}
/>
);

if (app.forum.attribute('allowPollOptionImage')) {
items.add(
'image',
<div className="Form-group">
<label className="label">{app.translator.trans('fof-polls.forum.modal.option_image.label') + ' #' + number}</label>
<UploadPollOptionImageButton
name={'pollOptionImage'}
poll={this.state.poll}
option={this.options[i]}
onUpload={this.pollImageUploadSuccess.bind(this)}
/>
<input type="hidden" name={'optionImage' + number} value={this.optionImage[i]} />
</div>
);

if (this.optionImage[i]()) {
items.add(
'image_alt',
<div className="Form-group">
<label className="label">{app.translator.trans('fof-polls.forum.modal.option_image.alt_label') + ' #' + number}</label>

<input type="text" required name={'optionImageAlt' + number} className="FormControl" bidi={this.optionImageAlt[i]} />

<p className="helpText">{app.translator.trans('fof-polls.forum.modal.poll_image.alt_help_text')}</p>
</div>
);
}
}

return items;
}

addOption() {
const max = Math.max(app.forum.attribute('pollMaxOptions'), 2);

if (this.options.length < max) {
this.options.push(app.store.createRecord('poll_options'));
this.optionAnswers.push(Stream(''));
this.optionImageUrls.push(Stream(''));
this.optionImage.push(Stream(''));
this.optionImageAlt.push(Stream(''));
} else {
alert(extractText(app.translator.trans('fof-polls.forum.modal.max', { max })));
}
Expand All @@ -307,7 +342,8 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
removeOption(i: number): void {
this.options.splice(i, 1);
this.optionAnswers.splice(i, 1);
this.optionImageUrls.splice(i, 1);
this.optionImage.splice(i, 1);
this.optionImageAlt.splice(i, 1);
}

data(): object {
Expand All @@ -323,7 +359,8 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
const options = this.options.map((option, i) => {
option.pushAttributes({
answer: this.optionAnswers[i](),
imageUrl: this.optionImageUrls[i](),
image: this.optionImage[i](),
imageAlt: this.optionImageAlt[i](),
});

return pollExists ? option.data : option.data.attributes;
Expand All @@ -332,7 +369,7 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
return {
question: this.question(),
subtitle: this.subtitle(),
pollImage: this.pollImage(),
pollImage: this.image(),
imageAlt: this.imageAlt(),
endDate: this.dateToTimestamp(this.endDate()) ?? false,
publicPoll: this.publicPoll(),
Expand Down Expand Up @@ -388,6 +425,6 @@ export default class PollForm extends Component<PollFormAttrs, PollFormState> {
}

pollImageUploadSuccess(fileName: string | null | undefined): void {
this.pollImage(fileName);
this.image(fileName);
}
}
6 changes: 3 additions & 3 deletions js/src/forum/components/UploadPollImageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface PollUploadObject {
fileName: string;
}

export default class UploadPollImageButton extends Button<UploadPollImageButtonAttrs> {
export default class UploadPollImageButton<CustomAttrs extends UploadPollImageButtonAttrs = UploadPollImageButtonAttrs> extends Button<CustomAttrs> {
loading: boolean = false;
uploadedImageUrl: string | undefined = undefined;
fileName: string | undefined = undefined;
Expand Down Expand Up @@ -66,7 +66,7 @@ export default class UploadPollImageButton extends Button<UploadPollImageButtonA
m.redraw();

app
.request({
.request<PollUploadObject>({
method: 'POST',
url: this.resourceUrl(),
serialize: (raw) => raw,
Expand All @@ -84,7 +84,7 @@ export default class UploadPollImageButton extends Button<UploadPollImageButtonA
m.redraw();

app
.request({
.request<PollUploadObject>({
method: 'DELETE',
url: this.resourceUrl(),
})
Expand Down
30 changes: 30 additions & 0 deletions js/src/forum/components/UploadPollOptionImageButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import app from 'flarum/forum/app';
import UploadPollImageButton, { UploadPollImageButtonAttrs } from './UploadPollImageButton';
import PollOption from '../models/PollOption';

export interface UploadPollOptionImageButtonAttrs extends UploadPollImageButtonAttrs {
option: PollOption;
}

export default class UploadPollOptionImageButton extends UploadPollImageButton<UploadPollOptionImageButtonAttrs> {
view(vnode: Mithril.Vnode<UploadPollOptionImageButtonAttrs>) {
const poll = this.attrs.poll;
if(poll?.exists) {

return super.view(vnode);
}

return <p className="UploadPollOptionImageButton-info">{app.translator.trans('fof-polls.forum.modal.option_image.requires_saved_poll')}</p>
}

resourceUrl() {
let url = app.forum.attribute('apiUrl') + '/fof/polls/pollOptionImage';
const poll = this.attrs.poll;
const option = this.attrs.option;

if (poll?.exists) url += '/' + poll?.id();
if (option?.exists) url += '/' + option?.id();

return url;
}
}
4 changes: 4 additions & 0 deletions js/src/forum/models/Poll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export default class Poll extends Model {
return Model.attribute<string | null>('subtitle').call(this);
}

image() {
return Model.attribute<string | null>('image').call(this);
}

imageUrl() {
return Model.attribute<string | null>('imageUrl').call(this);
}
Expand Down
8 changes: 8 additions & 0 deletions js/src/forum/models/PollOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ export default class PollOption extends Model {
return Model.attribute<string>('answer').call(this);
}

image() {
return Model.attribute<string | null>('image').call(this);
}

imageUrl() {
return Model.attribute<string>('imageUrl').call(this);
}

imageAlt() {
return Model.attribute<string | null>('imageAlt').call(this);
}

voteCount() {
return Model.attribute<number>('voteCount').call(this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/*
* This file is part of fof/polls.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Flarum\Database\Migration;

return Migration::renameColumn('poll_options', 'image_url', 'image');
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of fof/polls.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;

return [
'up' => function (Builder $schema) {
$schema->table('poll_options', function (Blueprint $table) {
$table->string('image_alt')->nullable()->after('image');
});
},
'down' => function (Builder $schema) {
$schema->table('poll_options', function (Blueprint $table) {
$table->dropColumn('image_alt');
});
},
];
5 changes: 5 additions & 0 deletions resources/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ fof-polls:
help: Upload an image to be displayed alongside the poll (optional).
alt_label: Image Alt Text
alt_help_text: This text is required when an image is set, it will be displayed if the image fails to load.
option_image:
label: Option Image
alt_label: Image Alt Text
alt_help_text: This text is required when an image is set, it will be displayed if the image fails to load.
requires_saved_poll: You must save the poll before adding image to the option.

moderation:
add: Add Poll
Expand Down
5 changes: 3 additions & 2 deletions src/Api/Controllers/DeletePollImageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ public function __construct(Factory $filesystemFactory)

public function handle(ServerRequestInterface $request): ResponseInterface
{
$actor = RequestUtil::getActor($request);
$pollId = Arr::get($request->getQueryParams(), 'pollId');

$poll = Poll::find($pollId);

$actor = RequestUtil::getActor($request);
$actor->assertCan('edit', $poll);

$this->uploadDir->delete($poll->image);

$poll->image = null;
Expand Down
Loading

0 comments on commit bcd2e69

Please sign in to comment.