Skip to content

Commit

Permalink
Enable discussion selection on profile page
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkwinkelmann committed May 24, 2022
1 parent b3da09c commit d604963
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 105 deletions.
2 changes: 1 addition & 1 deletion js/dist/forum.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/forum.js.map

Large diffs are not rendered by default.

131 changes: 29 additions & 102 deletions js/src/forum/addDiscussionControls.ts
Original file line number Diff line number Diff line change
@@ -1,124 +1,55 @@
import {extend} from 'flarum/common/extend';
import app from 'flarum/forum/app';
import listItems from 'flarum/common/helpers/listItems';
import ItemList from 'flarum/common/utils/ItemList';
import IndexPage from 'flarum/forum/components/IndexPage';
import DiscussionListItem from 'flarum/forum/components/DiscussionListItem';
import Button from 'flarum/common/components/Button';
import ItemList from 'flarum/common/utils/ItemList';
import icon from 'flarum/common/helpers/icon';
import listItems from 'flarum/common/helpers/listItems';
import Discussion from 'flarum/common/models/Discussion';
import DiscussionsUserPage from 'flarum/forum/components/DiscussionsUserPage';
import Checkbox, {CheckboxAttrs} from './components/Checkbox';
import SelectState from './utils/SelectState';
import discussionActionControls from './utils/discussionActionControls';
import discussionViewControls from './utils/discussionViewControls';

export default function () {
extend(IndexPage.prototype, 'oninit', function () {
app.current.set('mass-select', new SelectState('discussions'));
});

extend(IndexPage.prototype, 'viewItems', function (items) {
if (!app.forum.attribute('massControls')) {
return;
}

const controls = new ItemList();

let iconName = 'far fa-square';
const count = app.current.get('mass-select')!.count();
discussionViewControls(items);
});

if (count > 0) {
if (count === app.discussions.getPages().reduce((total, page) => total + page.items.length, 0)) {
iconName = 'fas fa-check-square';
} else {
iconName = 'fas fa-minus-square';
}
extend(IndexPage.prototype, 'actionItems', function (items) {
if (!app.forum.attribute('massControls')) {
return;
}

controls.add('all', Button.component({
onclick() {
app.current.get('mass-select')!.addAll();
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.all')));

controls.add('clear', Button.component({
onclick() {
app.current.get('mass-select')!.clear();
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.none')));

controls.add('read', Button.component({
onclick() {
app.current.get('mass-select')!.clear();
app.current.get('mass-select')!.addAll(model => {
return (model as Discussion).isRead();
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.read')));

controls.add('unread', Button.component({
onclick() {
app.current.get('mass-select')!.clear();
app.current.get('mass-select')!.addAll(model => {
return (model as Discussion).isUnread();
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.unread')));

controls.add('visible', Button.component({
onclick() {
app.current.get('mass-select')!.clear();
app.current.get('mass-select')!.addAll(model => {
return !(model as Discussion).isHidden();
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.visible')));

controls.add('hidden', Button.component({
onclick() {
app.current.get('mass-select')!.clear();
app.current.get('mass-select')!.addAll(model => {
return (model as Discussion).isHidden();
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.hidden')));
discussionActionControls(items);
});

if ('flarum-lock' in flarum.extensions) {
controls.add('locked', Button.component({
onclick() {
app.current.get('mass-select')!.clear();
app.current.get('mass-select')!.addAll(model => {
return model.attribute('isLocked');
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.locked')));
extend(DiscussionsUserPage.prototype, 'show', function () {
app.current.set('mass-select', new SelectState('discussions'));
app.current.get('mass-select')!.setListState(this.state!);
});

controls.add('unlocked', Button.component({
onclick() {
app.current.get('mass-select')!.clear();
app.current.get('mass-select')!.addAll(model => {
return !model.attribute('isLocked');
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.select.unlocked')));
extend(DiscussionsUserPage.prototype, 'content', function (vdom) {
if (!app.forum.attribute('massControls')) {
return;
}

// We don't use Flarum's SplitDropdown because the children of the first button aren't redrawing properly
items.add('mass-select', m('.ButtonGroup.Dropdown.Dropdown--split.dropdown', {}, [
Button.component({
className: 'Button SplitDropdown-button MassSelectControl' + (count > 0 ? ' checked' : ''),
onclick() {
if (app.current.get('mass-select')!.count() === 0) {
app.current.get('mass-select')!.addAll();
} else {
app.current.get('mass-select')!.clear();
}
},
}, icon(iconName)),
m('button.Dropdown-toggle.Button.Button--icon', {
'data-toggle': 'dropdown',
}, icon('fas fa-caret-down', {className: 'Button-caret'})),
m('ul.Dropdown-menu.dropdown-menu', listItems(controls.toArray())),
]), 100);
vdom.children.unshift(m('.IndexPage-toolbar', [
m('.IndexPage-toolbar-view', listItems(discussionViewControls(new ItemList()).toArray())),
m('.IndexPage-toolbar-action', listItems(discussionActionControls(new ItemList()).toArray())),
]));
});

extend(DiscussionListItem.prototype, 'view', function (vdom) {
// Only add the checkboxes on the index page, not the drawer
if (!app.current.matches(IndexPage) || !app.forum.attribute('massControls')) {
// Only add the checkboxes on the index+user pages, not the drawer
if (!(app.current.matches(IndexPage) || app.current.matches(DiscussionsUserPage)) || !app.forum.attribute('massControls')) {
return;
}

Expand All @@ -145,8 +76,4 @@ export default function () {
attrs.className += ' DiscussionListItem--selected';
}
});

extend(IndexPage.prototype, 'oninit', function () {
app.current.set('mass-select', new SelectState('discussions'));
});
}
10 changes: 9 additions & 1 deletion js/src/forum/utils/SelectState.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import Model from 'flarum/common/Model';
import app from 'flarum/forum/app';
import PaginatedListState from 'flarum/common/states/PaginatedListState';

export default class SelectState {
type: string
ids: string[] = []
rangeStartId: string | null = null
listState: PaginatedListState<Model> | null = null

constructor(type: string) {
this.type = type;
Expand Down Expand Up @@ -78,13 +80,19 @@ export default class SelectState {
private allCandidates(): Model[] {
const items: Model[] = [];

app.discussions.getPages().forEach(page => {
const state = this.listState || app.discussions;

state.getPages().forEach(page => {
items.push(...page.items);
});

return items;
}

setListState(state: PaginatedListState<Model>) {
this.listState = state;
}

addAll(filter: (model: Model) => boolean = () => true): void {
this.allCandidates().forEach(model => {
if (filter(model)) {
Expand Down
192 changes: 192 additions & 0 deletions js/src/forum/utils/discussionActionControls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import app from 'flarum/forum/app';
import Button from 'flarum/common/components/Button';
import Dropdown from 'flarum/common/components/Dropdown';
import DiscussionControls from 'flarum/forum/utils/DiscussionControls';
import Discussion from 'flarum/common/models/Discussion';
import extractText from 'flarum/common/utils/extractText';
import ItemList from 'flarum/common/utils/ItemList';
import proxyModels from './proxyModels';
import IconButton from '../components/IconButton';

export default function (items: ItemList<any>): ItemList<any> {
const select = app.current.get('mass-select');
if (!select || select.count() === 0) {
return items;
}

// Remove global actions
items.remove('refresh');
items.remove('markAllAsRead');

items.add('mass-markAsRead', m(IconButton, {
title: app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.markAsRead'),
icon: 'fas fa-check',
onclick() {
select.forEachPromise<Discussion>(discussion => {
// Same code as in DiscussionListItem
if (discussion.isUnread()) {
return discussion.save({lastReadPostNumber: discussion.lastPostNumber()});
}

return Promise.resolve();
}).then(() => {
m.redraw();
});
},
}));

const anyHidden = select.some<Discussion>(discussion => {
return discussion.isHidden();
});

if (app.forum.attribute('canHideDiscussionsSometime')) {
items.add('mass-hide', m(IconButton, {
title: anyHidden ? app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.restore') : app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.hide'),
icon: anyHidden ? 'fas fa-reply' : 'fas fa-trash-alt',
onclick() {
select.forEachPromise<Discussion>(discussion => {
if (!discussion.canHide()) {
return Promise.resolve();
}

if (anyHidden) {
return DiscussionControls.restoreAction.call(discussion);
} else {
return DiscussionControls.hideAction.call(discussion);
}
}).then(() => {
m.redraw();
});
},
disabled: !select.some<Discussion>(discussion => {
return !!discussion.canHide();
}),
}));
}

if (app.forum.attribute('canDeleteDiscussionsSometime') && anyHidden) {
// Make a more accurate count of what exactly we will attempt to delete
// so the confirmation message is not misleading
const count = select.all<Discussion>().filter(discussion => discussion.canDelete() && discussion.isHidden()).length;

items.add('mass-delete', m(IconButton, {
title: app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.delete'),
icon: 'fas fa-times',
onclick() {
// We can't call DiscussionControls.deleteAction as it would show a confirmation message for every selected discussion
// Instead we manually do the same thing with a single confirmation
if (!confirm(extractText(app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.deleteConfirmation', {
count,
})))) {
return;
}

select.forEachPromise<Discussion>(discussion => {
if (!discussion.canDelete() || !discussion.isHidden()) {
return Promise.resolve();
}

return discussion.delete().then(() => app.discussions.removeDiscussion(discussion));
}).then(() => {
m.redraw();
});
},
disabled: count === 0,
}));
}

if (app.forum.attribute('canLockDiscussionsSometime')) {
const anyLocked = select.some(discussion => {
return discussion.attribute('isLocked');
});

items.add('mass-lock', m(IconButton, {
title: anyLocked ? app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.unlock') : app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.lock'),
icon: anyLocked ? 'fas fa-unlock' : 'fas fa-lock',
onclick() {
select.forEachPromise(discussion => {
if (!discussion.attribute('canLock')) {
return Promise.resolve();
}

// Re-implement DiscussionControls.lockAction to force lock or unlock instead of toggling
return discussion.save({isLocked: !anyLocked});
}).then(() => {
m.redraw();
});
},
disabled: !select.some(discussion => {
return discussion.attribute('canLock');
}),
}));
}

if (app.forum.attribute('canStickyDiscussionsSometime')) {
const anySticky = select.some(discussion => {
return discussion.attribute('isSticky');
});

items.add('mass-sticky', m(IconButton, {
title: anySticky ? app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.unsticky') : app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.sticky'),
icon: 'fas fa-thumbtack', // Unfortunately, there is no good alternate icon for on/off
onclick() {
select.forEachPromise(discussion => {
if (!discussion.attribute('canSticky')) {
return Promise.resolve();
}

return discussion.save({isSticky: !anySticky});
}).then(() => {
m.redraw();
});
},
disabled: !select.some(discussion => {
return discussion.attribute('canSticky');
}),
}));
}

if (app.forum.attribute('canTagDiscussionsSometime')) {
items.add('mass-tags', Dropdown.component({
buttonClassName: 'Button',
label: app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.tags'),
disabled: !select.some(discussion => {
return discussion.attribute('canTag');
}),
}, [
Button.component({
onclick() {
app.modal.show(flarum.core.compat['tags/forum/components/TagDiscussionModal'], {
discussion: proxyModels(select.all()),
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.tags-edit')),
app.store.all('tags').map(tag => Button.component({
onclick() {
select.forEachPromise(discussion => {
if (!discussion.attribute('canTag')) {
return Promise.resolve();
}

const tags = discussion.tags() || [];

// If discussion already has this tag, skip
if (tags.some(thisTag => thisTag.id() === tag.id())) {
return Promise.resolve();
}

tags.push(tag);

return discussion.save({relationships: {tags}})
}).then(() => {
m.redraw();
});
},
}, app.translator.trans('clarkwinkelmann-mass-actions.forum.actions.tags-add', {
tag: tag.name(),
}))),
]));
}

return items;
}
Loading

0 comments on commit d604963

Please sign in to comment.