From 4b461a5af83b4c59c868451bebff135c8b0512e2 Mon Sep 17 00:00:00 2001 From: IanM Date: Tue, 30 Jan 2024 08:16:27 +0000 Subject: [PATCH] wip: add polls route, begin to setup state, controls, etc --- extend.php | 15 ++- js/src/forum/components/PollsDirectory.tsx | 148 +++++++++++++++++++++ js/src/forum/extend.ts | 3 +- js/src/forum/states/PollDirectoryState.ts | 135 +++++++++++++++++++ resources/views/directory/index.blade.php | 16 +++ src/Content/PollsDirectory.php | 83 ++++++++++++ 6 files changed, 394 insertions(+), 6 deletions(-) create mode 100644 js/src/forum/components/PollsDirectory.tsx create mode 100644 js/src/forum/states/PollDirectoryState.ts create mode 100644 resources/views/directory/index.blade.php create mode 100644 src/Content/PollsDirectory.php diff --git a/extend.php b/extend.php index e39fd879..041a9202 100755 --- a/extend.php +++ b/extend.php @@ -25,14 +25,15 @@ return [ (new Extend\Frontend('forum')) - ->js(__DIR__.'/js/dist/forum.js') - ->css(__DIR__.'/resources/less/forum.less'), + ->js(__DIR__ . '/js/dist/forum.js') + ->css(__DIR__ . '/resources/less/forum.less') + ->route('/polls', 'fof_polls_directory', Content\PollsDirectory::class), (new Extend\Frontend('admin')) - ->js(__DIR__.'/js/dist/admin.js') - ->css(__DIR__.'/resources/less/admin.less'), + ->js(__DIR__ . '/js/dist/admin.js') + ->css(__DIR__ . '/resources/less/admin.less'), - new Extend\Locales(__DIR__.'/resources/locale'), + new Extend\Locales(__DIR__ . '/resources/locale'), (new Extend\Routes('api')) ->post('/fof/polls', 'fof.polls.create', Controllers\CreatePollController::class) @@ -94,6 +95,7 @@ (new Extend\Settings()) ->default('fof-polls.maxOptions', 10) ->default('fof-polls.optionsColorBlend', true) + ->default('fof-polls.directory-default-sort', 'default') ->serializeToForum('allowPollOptionImage', 'fof-polls.allowOptionImage', 'boolval') ->serializeToForum('pollMaxOptions', 'fof-polls.maxOptions', 'intval') ->registerLessConfigVar('fof-polls-options-color-blend', 'fof-polls.optionsColorBlend', function ($value) { @@ -102,4 +104,7 @@ (new Extend\ModelVisibility(Poll::class)) ->scope(Access\ScopePollVisibility::class), + + (new Extend\View()) + ->namespace('fof-polls', __DIR__ . '/resources/views'), ]; diff --git a/js/src/forum/components/PollsDirectory.tsx b/js/src/forum/components/PollsDirectory.tsx new file mode 100644 index 00000000..bfd181c4 --- /dev/null +++ b/js/src/forum/components/PollsDirectory.tsx @@ -0,0 +1,148 @@ +import app from 'flarum/forum/app'; +import Page from 'flarum/common/components/Page'; +import extractText from 'flarum/common/utils/extractText'; +import IndexPage from 'flarum/forum/components/IndexPage'; +import ItemList from 'flarum/common/utils/ItemList'; +import listItems from 'flarum/common/helpers/listItems'; +import SelectDropdown from 'flarum/common/components/SelectDropdown'; +import LinkButton from 'flarum/common/components/LinkButton'; +import Select from 'flarum/common/components/Select'; +import Button from 'flarum/common/components/Button'; +import Mithril from 'mithril'; + +export default class PollsDirectory extends Page { + oncreate(vnode: Mithril.Vnode) { + super.oncreate(vnode); + + app.setTitle(extractText(app.translator.trans('fof-polls.forum.page.nav'))); + } + + view() { + return ( +
+ {IndexPage.prototype.hero()} +
+
+ +
+
+
    {listItems(this.viewItems().toArray())}
+
    {listItems(this.actionItems().toArray())}
+
+ {/* */} +
+
+
+
+ ); + } + + /** + * Our own sidebar. Re-uses Index.sidebarItems as the base + * Elements added here will only show up on the user directory page + */ + sidebarItems(): ItemList { + const items = IndexPage.prototype.sidebarItems(); + + items.setContent( + 'nav', + SelectDropdown.component( + { + buttonClassName: 'Button', + className: 'App-titleControl', + }, + this.navItems().toArray() + ) + ); + + return items; + } + + /** + * Our own sidebar navigation. Re-uses Index.navItems as the base + * Elements added here will only show up on the user directory page + */ + navItems(): ItemList { + const items = IndexPage.prototype.navItems(); + const params = this.stickyParams(); + + items.setContent( + 'fof-polls-directory', + LinkButton.component( + { + href: app.route('fof_polls_directory', params), + icon: 'fas fa-poll', + }, + app.translator.trans('fof-polls.forum.page.nav') + ), + ); + + return items; + } + + stickyParams() { + return { + sort: m.route.param('sort'), + q: m.route.param('q'), + }; + } + + changeParams(sort: string) { + const params = this.params(); + + if (sort === app.forum.attribute('pollsDirectoryDefaultSort')) { + delete params.sort; + } else { + params.sort = sort; + } + + this.state.refreshParams(params); + + const routeParams = { ...params }; + delete routeParams.qBuilder; + + m.route.set(app.route('fof_polls_directory', routeParams)); + } + + viewItems() { + const items = new ItemList(); + const sortMap = this.state.sortMap(); + + const sortOptions = {}; + for (const i in sortMap) { + sortOptions[i] = app.translator.trans('fof-polls.lib.sort.' + i); + } + + items.add( + 'sort', + Select.component({ + options: sortOptions, + value: this.state.getParams().sort || app.forum.attribute('pollsDirectoryDefaultSort'), + onchange: this.changeParams.bind(this), + }), + 100 + ); + + return items; + } + + actionItems(): ItemList { + const items = new ItemList(); + + items.add( + 'refresh', + Button.component({ + title: app.translator.trans('fof-polls.forum.page.refresh_tooltip'), + icon: 'fas fa-sync', + className: 'Button Button--icon', + onclick: () => { + this.state.refresh(); + }, + }) + ); + + return items; + } +} diff --git a/js/src/forum/extend.ts b/js/src/forum/extend.ts index 0df58e48..2f99a386 100644 --- a/js/src/forum/extend.ts +++ b/js/src/forum/extend.ts @@ -5,10 +5,11 @@ import Discussion from 'flarum/common/models/Discussion'; import Poll from './models/Poll'; import PollOption from './models/PollOption'; import PollVote from './models/PollVote'; +import PollsDirectory from './components/PollsDirectory'; export default [ new Extend.Routes() // - .add('fof_polls_directory', '/polls', 'polls'), + .add('fof_polls_directory', '/polls', PollsDirectory), new Extend.Store() // .add('polls', Poll) diff --git a/js/src/forum/states/PollDirectoryState.ts b/js/src/forum/states/PollDirectoryState.ts new file mode 100644 index 00000000..28446910 --- /dev/null +++ b/js/src/forum/states/PollDirectoryState.ts @@ -0,0 +1,135 @@ +import app from 'flarum/forum/app'; + +/** + * Based on Flarum's DiscussionListState + */ +import SortMap from '../../common/utils/SortMap'; + +export default class UserDirectoryState { + constructor(params = {}, app = window.app) { + this.params = params; + + this.app = app; + + this.users = []; + + this.moreResults = false; + + this.loading = false; + + this.qBuilder = {}; + } + + requestParams() { + const params = { include: [], filter: {} }; + + const sortKey = this.params.sort || app.forum.attribute('userDirectoryDefaultSort'); + + // sort might be set to null if no sort params has been passed + params.sort = this.sortMap()[sortKey]; + + if (this.params.q) { + params.filter.q = this.params.q; + } + + return params; + } + + sortMap() { + return { + default: '', + ...new SortMap().sortMap(), + }; + } + + getParams() { + return this.params; + } + + clear() { + this.users = []; + m.redraw(); + } + + refreshParams(newParams) { + if (!this.hasUsers() || Object.keys(newParams).some((key) => this.getParams()[key] !== newParams[key])) { + const q = ''; + this.params = newParams; + + if (newParams.qBuilder) { + Object.assign(this.qBuilder, newParams.qBuilder || {}); + this.params.q = Object.values(this.qBuilder).join(' ').trim(); + } + + if (!this.params.q && q) { + this.params.q = q; + } + + this.refresh(); + } + } + + refresh() { + this.loading = true; + + this.clear(); + + return this.loadResults().then( + (results) => { + this.users = []; + this.parseResults(results); + }, + () => { + this.loading = false; + m.redraw(); + } + ); + } + + loadResults(offset) { + const preloadedUsers = this.app.preloadedApiDocument(); + + if (preloadedUsers) { + return Promise.resolve(preloadedUsers); + } + + const params = this.requestParams(); + params.page = { offset }; + params.include = params.include.join(','); + + return this.app.store.find('users', params); + } + + loadMore() { + this.loading = true; + + this.loadResults(this.users.length).then(this.parseResults.bind(this)); + } + + parseResults(results) { + this.users.push(...results); + + this.loading = false; + this.moreResults = !!results.payload.links && !!results.payload.links.next; + + m.redraw(); + + return results; + } + + hasUsers() { + return this.users.length > 0; + } + + isLoading() { + return this.loading; + } + + isSearchResults() { + return !!this.params.q; + } + + empty() { + return !this.hasUsers() && !this.isLoading(); + } +} diff --git a/resources/views/directory/index.blade.php b/resources/views/directory/index.blade.php new file mode 100644 index 00000000..f40a0dfe --- /dev/null +++ b/resources/views/directory/index.blade.php @@ -0,0 +1,16 @@ + +
+

{{ $translator->trans('fof-polls.forum.page.nav') }}

+ +
    + @foreach ($apiDocument->data as $user) +
  • + {{ $user->attributes->username }} +
  • + @endforeach +
+ + {{ $translator->trans('core.views.index.next_page_button') }} » +
diff --git a/src/Content/PollsDirectory.php b/src/Content/PollsDirectory.php new file mode 100644 index 00000000..4fb6d2f2 --- /dev/null +++ b/src/Content/PollsDirectory.php @@ -0,0 +1,83 @@ + 'username', + 'username_za' => '-username', + 'newest' => '-joinedAt', + 'oldest' => 'joinedAt', + 'most_discussions' => '-discussionCount', + 'least_discussions' => 'discussionCount', + ]; + + public function __construct(Client $api, Factory $view, SettingsRepositoryInterface $settings) + { + $this->api = $api; + $this->view = $view; + $this->settings = $settings; + } + + private function getDocument(User $actor, array $params, ServerRequestInterface $request) + { + $actor->assertCan('seePollsList'); + + return json_decode($this->api->withQueryParams($params)->withParentRequest($request)->get('/fof/polls')->getBody()); + } + + public function __invoke(Document $document, ServerRequestInterface $request): Document + { + $queryParams = $request->getQueryParams(); + $actor = RequestUtil::getActor($request); + + $sort = Arr::pull($queryParams, 'sort') ?: $this->settings->get('fof-polls.directory-default-sort'); + $q = Arr::pull($queryParams, 'q'); + $page = Arr::pull($queryParams, 'page', 1); + + $params = [ + // ?? used to prevent null values. null would result in the whole sortMap array being sent in the params + 'sort' => Arr::get($this->sortMap, $sort ?? '', ''), + 'filter' => compact('q'), + 'page' => ['offset' => ($page - 1) * 20, 'limit' => 20], + ]; + + $apiDocument = $this->getDocument($actor, $params, $request); + + $document->content = $this->view->make('fof-polls::directory.index', compact('page', 'apiDocument')); + + $document->payload['apiDocument'] = $apiDocument; + + return $document; + } +}