From 3b45416b0618d35975fe29c4922f360552e433cc Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 21 Sep 2023 20:57:44 -0700 Subject: [PATCH 01/27] Stricter / more accurate query parsing, return params in response --- .../Controllers/GroupHistoryController.php | 20 +++++++++++++------ app/helpers.php | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/GroupHistoryController.php b/app/Http/Controllers/GroupHistoryController.php index 5d7361fcd9c..ffc134da9a9 100644 --- a/app/Http/Controllers/GroupHistoryController.php +++ b/app/Http/Controllers/GroupHistoryController.php @@ -16,11 +16,10 @@ public function index() { $rawParams = request()->all(); $params = get_params($rawParams, null, [ - 'group:string', + 'group:string_presence', 'max_date:time', 'min_date:time', - 'sort:string', - 'user:string', + 'user:string_presence', ], ['null_missing' => true]); $query = UserGroupEvent::visibleForUser(auth()->user()); @@ -36,11 +35,19 @@ public function index() } if ($params['max_date'] !== null) { + $params['max_date']->endOfDay(); + $query->where('created_at', '<=', $params['max_date']); + + $params['max_date'] = json_date($params['max_date']); } if ($params['min_date'] !== null) { + $params['min_date']->startOfDay(); + $query->where('created_at', '>=', $params['min_date']); + + $params['min_date'] = json_date($params['min_date']); } if ($params['user'] !== null) { @@ -53,16 +60,17 @@ public function index() } } - $cursorHelper = UserGroupEvent::makeDbCursorHelper($params['sort']); + $cursorHelper = UserGroupEvent::makeDbCursorHelper($rawParams['sort'] ?? null); + $params['sort'] = $cursorHelper->getSortName(); [$events, $hasMore] = $query ->cursorSort($cursorHelper, cursor_from_params($rawParams)) ->limit(50) ->getWithHasMore(); - $cursor = $cursorHelper->next($events, $hasMore); return [ + ...cursor_for_response($cursorHelper->next($events, $hasMore)), 'events' => json_collection($events, 'UserGroupEvent'), - ...cursor_for_response($cursor), + 'params' => $params, ]; } } diff --git a/app/helpers.php b/app/helpers.php index 9bc3b341c8e..54512262c6b 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1501,6 +1501,8 @@ function get_param_value($input, $type) return get_length($input); case 'string': return get_string($input); + case 'string_presence': + return presence(get_string($input)); case 'string_split': return get_string_split($input); case 'string[]': From 1dd4c09d1928fef984539ff563d53531df1e074f Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 21 Sep 2023 20:57:44 -0700 Subject: [PATCH 02/27] Return relevant groups in response This also includes all groups the user can normally view, for use in a group selector --- app/Http/Controllers/GroupHistoryController.php | 8 ++++++++ app/Libraries/OsuAuthorize.php | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/Http/Controllers/GroupHistoryController.php b/app/Http/Controllers/GroupHistoryController.php index ffc134da9a9..2b539412103 100644 --- a/app/Http/Controllers/GroupHistoryController.php +++ b/app/Http/Controllers/GroupHistoryController.php @@ -7,6 +7,7 @@ namespace App\Http\Controllers; +use App\Models\Group; use App\Models\User; use App\Models\UserGroupEvent; @@ -67,9 +68,16 @@ public function index() ->limit(50) ->getWithHasMore(); + $eventGroupIds = $events->pluck('group_id'); + $groups = app('groups')->all()->filter( + fn (Group $group) => + $eventGroupIds->contains($group->getKey()) || + priv_check('GroupShow', $group)->can(), + ); return [ ...cursor_for_response($cursorHelper->next($events, $hasMore)), 'events' => json_collection($events, 'UserGroupEvent'), + 'groups' => json_collection($groups, 'Group'), 'params' => $params, ]; } diff --git a/app/Libraries/OsuAuthorize.php b/app/Libraries/OsuAuthorize.php index 2ed38bad822..3fce00fe8d2 100644 --- a/app/Libraries/OsuAuthorize.php +++ b/app/Libraries/OsuAuthorize.php @@ -21,6 +21,7 @@ use App\Models\Forum\Topic; use App\Models\Forum\TopicCover; use App\Models\Genre; +use App\Models\Group; use App\Models\Language; use App\Models\LegacyMatch\LegacyMatch; use App\Models\Multiplayer\Room; @@ -1769,6 +1770,15 @@ public function checkForumTopicVote(?User $user, Topic $topic): string return 'ok'; } + public function checkGroupShow(?User $user, Group $group): string + { + if ($group->hasListing() || $user?->isGroup($group)) { + return 'ok'; + } + + return 'unauthorized'; + } + public function checkIsOwnClient(?User $user, Client $client): string { if ($user === null || $user->getKey() !== $client->user_id) { From 9eef9c32861cccca6dd5423e24e00e3a6b0cfcd5 Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 21 Sep 2023 20:57:44 -0700 Subject: [PATCH 03/27] Show actor for default group events only if user is privileged --- app/Libraries/OsuAuthorize.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Libraries/OsuAuthorize.php b/app/Libraries/OsuAuthorize.php index 3fce00fe8d2..a2e59ebaf07 100644 --- a/app/Libraries/OsuAuthorize.php +++ b/app/Libraries/OsuAuthorize.php @@ -1928,6 +1928,10 @@ public function checkScorePin(?User $user, ScoreBest|Solo\Score $score): string public function checkUserGroupEventShowActor(?User $user, UserGroupEvent $event): string { + if ($event->group->identifier === 'default') { + return $user?->isPrivileged() ? 'ok' : 'unauthorized'; + } + if ($user?->isGroup($event->group)) { return 'ok'; } From 94f2c2dd2ae6ee07c1546d4f582161ed492243de Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 21 Sep 2023 20:57:45 -0700 Subject: [PATCH 04/27] Include actor as null rather than all null values --- app/Transformers/UserGroupEventTransformer.php | 4 ++++ resources/js/interfaces/user-group-event-json.ts | 7 ++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/Transformers/UserGroupEventTransformer.php b/app/Transformers/UserGroupEventTransformer.php index 5075f6696c7..6cdf67010af 100644 --- a/app/Transformers/UserGroupEventTransformer.php +++ b/app/Transformers/UserGroupEventTransformer.php @@ -43,6 +43,10 @@ public function transform(UserGroupEvent $event): array public function includeActor(UserGroupEvent $event): ResourceInterface { + if ($event->actor_id === null) { + return $this->null(); + } + return $this->primitive([ 'id' => $event->actor_id, 'username' => $event->details['actor_name'], diff --git a/resources/js/interfaces/user-group-event-json.ts b/resources/js/interfaces/user-group-event-json.ts index 5bad925aa0b..da38de54fcf 100644 --- a/resources/js/interfaces/user-group-event-json.ts +++ b/resources/js/interfaces/user-group-event-json.ts @@ -6,11 +6,8 @@ import GameMode from './game-mode'; interface UserGroupEventBase { actor?: { id: number; - name: string; - } | { - id: null; - name: null; - }; + username: string; + } | null; created_at: string; group_id: number; group_name: string; From 16270f98d49aec27ce3271eda562bba1e0afe097 Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 21 Sep 2023 20:57:45 -0700 Subject: [PATCH 05/27] Add group history page --- .../Controllers/GroupHistoryController.php | 6 +- app/Libraries/RouteSection.php | 3 + package.json | 2 +- resources/css/bem-index.less | 3 + resources/css/bem/group-history-event.less | 65 +++++++++ .../css/bem/group-history-search-form.less | 63 ++++++++ resources/css/bem/group-history.less | 16 ++ resources/css/bem/osu-page.less | 10 ++ resources/css/bem/show-more-link.less | 3 +- resources/js/entrypoints/group-history.tsx | 11 ++ resources/js/group-history/event.tsx | 87 +++++++++++ resources/js/group-history/events.tsx | 29 ++++ resources/js/group-history/group-store.ts | 40 +++++ resources/js/group-history/json.ts | 18 +++ resources/js/group-history/main.tsx | 137 ++++++++++++++++++ resources/js/group-history/search-form.tsx | 131 +++++++++++++++++ resources/lang/en/group_history.php | 38 +++++ resources/lang/en/page_title.php | 3 + resources/views/group_history/index.blade.php | 19 +++ yarn.lock | 8 +- 20 files changed, 685 insertions(+), 7 deletions(-) create mode 100644 resources/css/bem/group-history-event.less create mode 100644 resources/css/bem/group-history-search-form.less create mode 100644 resources/css/bem/group-history.less create mode 100644 resources/js/entrypoints/group-history.tsx create mode 100644 resources/js/group-history/event.tsx create mode 100644 resources/js/group-history/events.tsx create mode 100644 resources/js/group-history/group-store.ts create mode 100644 resources/js/group-history/json.ts create mode 100644 resources/js/group-history/main.tsx create mode 100644 resources/js/group-history/search-form.tsx create mode 100644 resources/lang/en/group_history.php create mode 100644 resources/views/group_history/index.blade.php diff --git a/app/Http/Controllers/GroupHistoryController.php b/app/Http/Controllers/GroupHistoryController.php index 2b539412103..175edee4580 100644 --- a/app/Http/Controllers/GroupHistoryController.php +++ b/app/Http/Controllers/GroupHistoryController.php @@ -74,11 +74,15 @@ public function index() $eventGroupIds->contains($group->getKey()) || priv_check('GroupShow', $group)->can(), ); - return [ + $json = [ ...cursor_for_response($cursorHelper->next($events, $hasMore)), 'events' => json_collection($events, 'UserGroupEvent'), 'groups' => json_collection($groups, 'Group'), 'params' => $params, ]; + + return is_json_request() + ? $json + : ext_view('group_history.index', compact('json')); } } diff --git a/app/Libraries/RouteSection.php b/app/Libraries/RouteSection.php index af03c091baf..ef13fd82132 100644 --- a/app/Libraries/RouteSection.php +++ b/app/Libraries/RouteSection.php @@ -70,6 +70,9 @@ class RouteSection 'friends_controller' => [ '_' => 'home', ], + 'group_history_controller' => [ + '_' => 'home', + ], 'groups_controller' => [ '_' => 'home', ], diff --git a/package.json b/package.json index ad40c913b41..f8f3a49646a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@discordapp/twemoji": "^14.0.2", - "@fortawesome/fontawesome-free": "^5.6.3", + "@fortawesome/fontawesome-free": "^5.15.4", "@types/autosize": "^4.0.1", "@types/bootstrap": "^3.3.0", "@types/d3": "^7.1.0", diff --git a/resources/css/bem-index.less b/resources/css/bem-index.less index 71b4044667c..4a90ab34aff 100644 --- a/resources/css/bem-index.less +++ b/resources/css/bem-index.less @@ -178,6 +178,9 @@ @import "bem/game-mode"; @import "bem/game-mode-link"; @import "bem/grid-items"; +@import "bem/group-history"; +@import "bem/group-history-event"; +@import "bem/group-history-search-form"; @import "bem/header-buttons"; @import "bem/header-nav-mobile"; @import "bem/header-nav-v4"; diff --git a/resources/css/bem/group-history-event.less b/resources/css/bem/group-history-event.less new file mode 100644 index 00000000000..0fb5420754c --- /dev/null +++ b/resources/css/bem/group-history-event.less @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.group-history-event { + @_top: group-history-event; + + align-items: center; + display: flex; + font-size: @font-size--title-small; + gap: 15px; + + &__icon { + @icons: { + group-add: users; + group-remove: users-slash; + group-rename: users-cog; + user-add: user-plus; + user-add-playmodes: user-tag; + user-remove: user-minus; + user-remove-playmodes: user-tag; + user-set-default: user-cog; + }; + each(@icons, { + .@{_top}--@{key} & { + @icon-var: 'fa-var-@{value}'; + --icon: @@icon-var; + } + }); + + .fas(); + background-color: var(--group-colour, hsl(var(--hsl-b1))); + border-radius: 10000px; + color: hsl(var(--hsl-b6)); + padding: 3px 6px; + + &::before { + content: var(--icon); + } + } + + &__info { + align-items: end; + color: hsl(var(--hsl-f1)); + column-gap: 15px; + display: flex; + flex-direction: column; + flex-shrink: 0; + font-size: @font-size--normal; + + @media @desktop { + flex-direction: row-reverse; + } + } + + &__message { + @bold-events: group-add, group-remove, group-rename; + each(@bold-events, { + .@{_top}--@{value} & { + font-weight: bold; + } + }); + + flex-grow: 1; + } +} diff --git a/resources/css/bem/group-history-search-form.less b/resources/css/bem/group-history-search-form.less new file mode 100644 index 00000000000..03fa0f64a6f --- /dev/null +++ b/resources/css/bem/group-history-search-form.less @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.group-history-search-form { + --input-bg: hsl(var(--hsl-b5)); + --input-border-radius: @border-radius--large; + background: hsl(var(--hsl-b4)); + + &__content { + .default-gutter-v2(); + padding-bottom: 20px; + padding-top: 20px; + + &--buttons { + background-color: hsl(var(--hsl-b3)); + display: flex; + gap: 10px; + justify-content: center; + padding-bottom: 10px; + padding-top: 10px; + } + + &--inputs { + display: grid; + gap: 10px; + grid-template-columns: repeat(2, 1fr) repeat(2, 180px); + + @media @mobile { + grid-template-columns: repeat(2, 1fr); + + > :nth-child(-n + 2) { + grid-column: span 2; + } + } + } + } + + &__input { + .reset-input(); + font-size: @font-size--title-small-3; + width: 100%; + } + + &__label { + color: var(--label-colour); + padding-bottom: 5px; + } + + &__select-container { + position: relative; + + &::after { + .center-content(); + .fas(); + content: @fa-var-chevron-down; + height: 100%; + padding-left: 10px; + pointer-events: none; + position: absolute; + right: 5px; + } + } +} diff --git a/resources/css/bem/group-history.less b/resources/css/bem/group-history.less new file mode 100644 index 00000000000..73c26a514b7 --- /dev/null +++ b/resources/css/bem/group-history.less @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.group-history { + &__events { + display: flex; + flex-direction: column; + gap: 10px; + } + + &__none { + font-size: @font-size--title-small-3; + margin: 0; + text-align: center; + } +} diff --git a/resources/css/bem/osu-page.less b/resources/css/bem/osu-page.less index b8eec4cd564..a14993c6508 100644 --- a/resources/css/bem/osu-page.less +++ b/resources/css/bem/osu-page.less @@ -188,6 +188,16 @@ .default(); } + &--group-history-footer { + .default(); + .default-gutter-v2(); + background-color: hsl(var(--hsl-b4)); + font-size: @font-size--normal; + padding-bottom: 10px; + padding-top: 10px; + text-align: center; + } + &--info-bar { .default-gutter-v2(); padding-top: 5px; diff --git a/resources/css/bem/show-more-link.less b/resources/css/bem/show-more-link.less index 7e60b002891..226815b2e71 100644 --- a/resources/css/bem/show-more-link.less +++ b/resources/css/bem/show-more-link.less @@ -38,7 +38,8 @@ margin: 40px 0; } - &--chat-conversation-earlier-messages { + &--chat-conversation-earlier-messages, + &--group-history { margin: 20px auto 0; } diff --git a/resources/js/entrypoints/group-history.tsx b/resources/js/entrypoints/group-history.tsx new file mode 100644 index 00000000000..e94d0adac6c --- /dev/null +++ b/resources/js/entrypoints/group-history.tsx @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import Main from 'group-history/main'; +import core from 'osu-core-singleton'; +import * as React from 'react'; +import { parseJson } from 'utils/json'; + +core.reactTurbolinks.register('group-history', () => ( +
+)); diff --git a/resources/js/group-history/event.tsx b/resources/js/group-history/event.tsx new file mode 100644 index 00000000000..d2bfab3068f --- /dev/null +++ b/resources/js/group-history/event.tsx @@ -0,0 +1,87 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import StringWithComponent from 'components/string-with-component'; +import TimeWithTooltip from 'components/time-with-tooltip'; +import UserLink from 'components/user-link'; +import UserGroupEventJson from 'interfaces/user-group-event-json'; +import { route } from 'laroute'; +import { kebabCase } from 'lodash'; +import * as React from 'react'; +import { classWithModifiers, groupColour } from 'utils/css'; +import { trans, transArray } from 'utils/lang'; +import groupStore from './group-store'; + +interface Props { + event: UserGroupEventJson; +} + +export default class Event extends React.PureComponent { + private get messageMappings() { + const event = this.props.event; + const mappings: Record = { + group: ( + + {event.group_name} + + ), + }; + + if ('playmodes' in event && event.playmodes != null) { + mappings.playmodes = transArray( + event.playmodes.map((mode) => trans(`beatmaps.mode.${mode}`)), + ); + } + + if ('previous_group_name' in event) { + mappings.previous_group = ( + + {event.previous_group_name} + + ); + } + + if (event.user_id != null) { + mappings.user = ; + } + + return mappings; + } + + private get messagePattern() { + const event = this.props.event; + const type = event.type === 'user_add' && event.playmodes != null + ? 'user_add_with_playmodes' + : event.type; + + return trans(`group_history.event.message.${type}`); + } + + render() { + return ( +
+ +
+ +
+
+ + {this.props.event.actor != null && ( + + }} + pattern={trans('group_history.event.actor')} + /> + + )} +
+
+ ); + } +} diff --git a/resources/js/group-history/events.tsx b/resources/js/group-history/events.tsx new file mode 100644 index 00000000000..86aa5524594 --- /dev/null +++ b/resources/js/group-history/events.tsx @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import UserGroupEventJson from 'interfaces/user-group-event-json'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { trans } from 'utils/lang'; +import Event from './event'; + +interface Props { + events: UserGroupEventJson[]; +} + +@observer +export default class Events extends React.Component { + render() { + return this.props.events.length > 0 ? ( +
+ {this.props.events.map((event) => ( + + ))} +
+ ) : ( +

+ {trans('group_history.none')} +

+ ); + } +} diff --git a/resources/js/group-history/group-store.ts b/resources/js/group-history/group-store.ts new file mode 100644 index 00000000000..b97637daa1f --- /dev/null +++ b/resources/js/group-history/group-store.ts @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import GroupJson from 'interfaces/group-json'; +import { sortBy } from 'lodash'; +import { action, computed, makeObservable, observable } from 'mobx'; + +class GroupStore { + @observable byId = observable.map(); + + @computed + get all() { + return sortBy([...this.byId.values()], 'name'); + } + + @computed + get byIdentifier() { + const byIdentifier = new Map(); + + for (const group of this.byId.values()) { + byIdentifier.set(group.identifier, group); + } + + return byIdentifier; + } + + constructor() { + makeObservable(this); + } + + @action + update(groups: GroupJson[]): void { + for (const group of groups) { + this.byId.set(group.id, group); + } + } +} + +const groupStore = new GroupStore(); +export default groupStore; diff --git a/resources/js/group-history/json.ts b/resources/js/group-history/json.ts new file mode 100644 index 00000000000..37d91c95bb8 --- /dev/null +++ b/resources/js/group-history/json.ts @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import GroupJson from 'interfaces/group-json'; +import UserGroupEventJson from 'interfaces/user-group-event-json'; + +export default interface GroupHistoryJson { + cursor_string: string | null; + events: UserGroupEventJson[]; + groups: GroupJson[]; + params: { + group: string | null; + max_date: string | null; + min_date: string | null; + sort: string; + user: string | null; + }; +} diff --git a/resources/js/group-history/main.tsx b/resources/js/group-history/main.tsx new file mode 100644 index 00000000000..af6a7ed4420 --- /dev/null +++ b/resources/js/group-history/main.tsx @@ -0,0 +1,137 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import HeaderV4 from 'components/header-v4'; +import ShowMoreLink from 'components/show-more-link'; +import StringWithComponent from 'components/string-with-component'; +import UserGroupEventJson from 'interfaces/user-group-event-json'; +import { route } from 'laroute'; +import { IReactionDisposer, action, autorun, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { onErrorWithCallback } from 'utils/ajax'; +import { trans } from 'utils/lang'; +import { updateQueryString, wikiUrl } from 'utils/url'; +import Events from './events'; +import groupStore from './group-store'; +import GroupHistoryJson from './json'; +import SearchForm from './search-form'; + +@observer +export default class Main extends React.Component { + @observable private currentParams: GroupHistoryJson['params']; + @observable private cursorString: string | null; + private disposeQueryStringUpdater: IReactionDisposer; + @observable private events: UserGroupEventJson[]; + @observable private loading?: 'more' | 'new'; + @observable private newParams: GroupHistoryJson['params']; + private xhr?: JQuery.jqXHR; + + constructor(props: GroupHistoryJson) { + super(props); + + groupStore.update(props.groups); + this.currentParams = props.params; + this.cursorString = props.cursor_string; + this.events = props.events; + this.newParams = { ...this.currentParams }; + + // Update the query string of the URL whenever the current params are + // modified. Remove "sort" from the query if it's set to the default + this.disposeQueryStringUpdater = autorun(() => { + const params = { + ...this.currentParams, + sort: this.currentParams.sort === 'id_desc' ? null : this.currentParams.sort, + }; + + history.replaceState(history.state, '', updateQueryString(null, params)); + }); + + // If the "group" param doesn't match any group we can show as a select + // option, set it to null in the new params. This prevents the new params + // from initially being out of sync with the displayed form controls + if (props.params.group != null && !groupStore.byIdentifier.has(props.params.group)) { + this.newParams.group = null; + } + + makeObservable(this); + } + + componentWillUnmount() { + this.disposeQueryStringUpdater(); + this.xhr?.abort(); + } + + render() { + return ( + <> + +
+ +
+
+ + +
+
+ + {trans('group_history.staff_log.wiki_articles')} + + ), + }} + pattern={trans('group_history.staff_log._')} + /> +
+ + ); + } + + @action + private loadEvents(params: GroupHistoryJson['params'], cursorString?: string) { + this.xhr?.abort(); + this.loading = cursorString == null ? 'new' : 'more'; + + this.xhr = $.ajax( + route('group-history.index'), + { + data: { ...params, cursor_string: cursorString }, + dataType: 'json', + method: 'GET', + }, + ) + .done(action((response: GroupHistoryJson) => { + groupStore.update(response.groups); + this.currentParams = response.params; + this.cursorString = response.cursor_string; + + if (cursorString == null) { + this.events = response.events; + } else { + this.events.push(...response.events); + } + })) + .fail(onErrorWithCallback(() => this.loadEvents(params, cursorString))) + .always(action(() => this.loading = undefined)); + } + + private readonly onSearch = () => this.loadEvents(this.newParams); + + private readonly onShowMore = () => { + if (this.cursorString != null) { + this.loadEvents(this.currentParams, this.cursorString); + } + }; +} diff --git a/resources/js/group-history/search-form.tsx b/resources/js/group-history/search-form.tsx new file mode 100644 index 00000000000..279c1d9fa65 --- /dev/null +++ b/resources/js/group-history/search-form.tsx @@ -0,0 +1,131 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import BigButton from 'components/big-button'; +import InputContainer from 'components/input-container'; +import { action, makeObservable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { trans } from 'utils/lang'; +import groupStore from './group-store'; +import GroupHistoryJson from './json'; + +const bn = 'group-history-search-form'; +const formParamKeys = ['group', 'max_date', 'min_date', 'user'] as const; + +interface Props { + currentParams: GroupHistoryJson['params']; + loading: boolean; + newParams: GroupHistoryJson['params']; + onSearch: () => void; +} + +@observer +export default class SearchForm extends React.Component { + constructor(props: Props) { + super(props); + makeObservable(this); + } + + render() { + const newParamsEmpty = formParamKeys.every( + (key) => this.props.newParams[key] == null, + ); + const newParamsSame = formParamKeys.every( + (key) => this.props.newParams[key] === this.props.currentParams[key], + ); + + return ( +
+
+ +
+ +
+
+ + + + + + + + + +
+
+ + +
+
+ ); + } + + @action + private readonly onChange = (event: React.ChangeEvent) => { + event.preventDefault(); + + this.props.newParams[event.currentTarget.name as (typeof formParamKeys)[number]] = + event.currentTarget.value || null; + }; + + @action + private readonly onReset = (event: React.MouseEvent) => { + event.preventDefault(); + + for (const key of formParamKeys) { + this.props.newParams[key] = null; + } + }; + + private readonly onSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + this.props.onSearch(); + }; +} diff --git a/resources/lang/en/group_history.php b/resources/lang/en/group_history.php new file mode 100644 index 00000000000..d66481e6319 --- /dev/null +++ b/resources/lang/en/group_history.php @@ -0,0 +1,38 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +return [ + 'none' => 'No group history found!', + + 'event' => [ + 'actor' => 'by :user', + + 'message' => [ + 'group_add' => ':group created.', + 'group_remove' => ':group deleted.', + 'group_rename' => ':previous_group renamed to :group.', + 'user_add' => ':user added to :group.', + 'user_add_with_playmodes' => ':user added to :group for :playmodes.', + 'user_add_playmodes' => ':playmodes added to :user\'s :group membership.', + 'user_remove' => ':user removed from :group.', + 'user_remove_playmodes' => ':playmodes removed from :user\'s :group membership.', + 'user_set_default' => ':user\'s default group set to :group.', + ], + ], + + 'form' => [ + 'group' => 'Group', + 'group_all' => 'All groups', + 'max_date' => 'To', + 'min_date' => 'From', + 'user' => 'User', + 'user_prompt' => 'Username or ID', + ], + + 'staff_log' => [ + '_' => 'Older group history can be found in :wiki_articles.', + 'wiki_articles' => 'the staff log wiki articles', + ], +]; diff --git a/resources/lang/en/page_title.php b/resources/lang/en/page_title.php index 8d627192274..ce369e7ac1b 100644 --- a/resources/lang/en/page_title.php +++ b/resources/lang/en/page_title.php @@ -66,6 +66,9 @@ 'contests_controller' => [ '_' => 'contests', ], + 'group_history_controller' => [ + '_' => 'group history', + ], 'groups_controller' => [ 'show' => 'groups', ], diff --git a/resources/views/group_history/index.blade.php b/resources/views/group_history/index.blade.php new file mode 100644 index 00000000000..d235a94efa3 --- /dev/null +++ b/resources/views/group_history/index.blade.php @@ -0,0 +1,19 @@ +{{-- + Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. + See the LICENCE file in the repository root for full licence text. +--}} +@extends('master') + +@section('content') +
+@endsection + +@section("script") + @parent + + + + @include('layout._react_js', ['src' => 'js/group-history.js']) +@endsection diff --git a/yarn.lock b/yarn.lock index a556b26c47f..c1c226a049f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -238,10 +238,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@fortawesome/fontawesome-free@^5.6.3": - version "5.9.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.9.0.tgz#1aa5c59efb1b8c6eb6277d1e3e8c8f31998b8c8e" - integrity sha512-g795BBEzM/Hq2SYNPm/NQTIp3IWd4eXSH0ds87Na2jnrAUFX3wkyZAI4Gwj9DOaWMuz2/01i8oWI7P7T/XLkhg== +"@fortawesome/fontawesome-free@^5.15.4": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5" + integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg== "@istanbuljs/schema@^0.1.2": version "0.1.2" From 80eb4e3557e7644e9618e29f18b3f07431f5b223 Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 21 Sep 2023 20:57:45 -0700 Subject: [PATCH 06/27] Fix test name lol --- tests/Controllers/GroupHistoryControllerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Controllers/GroupHistoryControllerTest.php b/tests/Controllers/GroupHistoryControllerTest.php index f09ab0907eb..0a6da95043a 100644 --- a/tests/Controllers/GroupHistoryControllerTest.php +++ b/tests/Controllers/GroupHistoryControllerTest.php @@ -56,7 +56,7 @@ public function testIndexIncludesHiddenEventsWhenInGroup(): void $this->assertContains($event->getKey(), $responseEventIds); } - public function textIndexListsEvents(): void + public function testIndexListsEvents(): void { $event = UserGroupEvent::factory()->create(); From 9588b1d4f8e4e4c53286e01e40e7dd4b3419fffa Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 21 Sep 2023 23:02:05 -0700 Subject: [PATCH 07/27] Remove duplicate get_params type --- app/Http/Controllers/GroupHistoryController.php | 4 ++-- app/helpers.php | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/GroupHistoryController.php b/app/Http/Controllers/GroupHistoryController.php index 175edee4580..601952475f8 100644 --- a/app/Http/Controllers/GroupHistoryController.php +++ b/app/Http/Controllers/GroupHistoryController.php @@ -17,10 +17,10 @@ public function index() { $rawParams = request()->all(); $params = get_params($rawParams, null, [ - 'group:string_presence', + 'group', 'max_date:time', 'min_date:time', - 'user:string_presence', + 'user', ], ['null_missing' => true]); $query = UserGroupEvent::visibleForUser(auth()->user()); diff --git a/app/helpers.php b/app/helpers.php index 54512262c6b..9bc3b341c8e 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1501,8 +1501,6 @@ function get_param_value($input, $type) return get_length($input); case 'string': return get_string($input); - case 'string_presence': - return presence(get_string($input)); case 'string_split': return get_string_split($input); case 'string[]': From cf2121e44f3726ddc8463a895cdf50f9598b500e Mon Sep 17 00:00:00 2001 From: clayton Date: Sat, 24 Feb 2024 12:01:26 -0800 Subject: [PATCH 08/27] Rename playmodes to rulesets where possible --- resources/js/group-history/event.tsx | 2 +- resources/lang/en/group_history.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/js/group-history/event.tsx b/resources/js/group-history/event.tsx index d2bfab3068f..d8f948dc94b 100644 --- a/resources/js/group-history/event.tsx +++ b/resources/js/group-history/event.tsx @@ -28,7 +28,7 @@ export default class Event extends React.PureComponent { }; if ('playmodes' in event && event.playmodes != null) { - mappings.playmodes = transArray( + mappings.rulesets = transArray( event.playmodes.map((mode) => trans(`beatmaps.mode.${mode}`)), ); } diff --git a/resources/lang/en/group_history.php b/resources/lang/en/group_history.php index d66481e6319..fcbc64bf16b 100644 --- a/resources/lang/en/group_history.php +++ b/resources/lang/en/group_history.php @@ -14,10 +14,10 @@ 'group_remove' => ':group deleted.', 'group_rename' => ':previous_group renamed to :group.', 'user_add' => ':user added to :group.', - 'user_add_with_playmodes' => ':user added to :group for :playmodes.', - 'user_add_playmodes' => ':playmodes added to :user\'s :group membership.', + 'user_add_with_playmodes' => ':user added to :group for :rulesets.', + 'user_add_playmodes' => ':rulesets added to :user\'s :group membership.', 'user_remove' => ':user removed from :group.', - 'user_remove_playmodes' => ':playmodes removed from :user\'s :group membership.', + 'user_remove_playmodes' => ':rulesets removed from :user\'s :group membership.', 'user_set_default' => ':user\'s default group set to :group.', ], ], From 731d16a0322c05d5d3cdf28dbddeaa2c76ed4462 Mon Sep 17 00:00:00 2001 From: clayton Date: Sun, 25 Feb 2024 19:11:23 -0800 Subject: [PATCH 09/27] Normal map observable for group store --- resources/js/group-history/group-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/group-history/group-store.ts b/resources/js/group-history/group-store.ts index b97637daa1f..9997d32ef64 100644 --- a/resources/js/group-history/group-store.ts +++ b/resources/js/group-history/group-store.ts @@ -6,7 +6,7 @@ import { sortBy } from 'lodash'; import { action, computed, makeObservable, observable } from 'mobx'; class GroupStore { - @observable byId = observable.map(); + @observable byId = new Map(); @computed get all() { From 634f4dc9cc2018557e8635f8480247a0368b12b1 Mon Sep 17 00:00:00 2001 From: clayton Date: Mon, 20 May 2024 23:29:30 -0700 Subject: [PATCH 10/27] Make group-history style options modifiers instead of elements --- resources/css/bem/group-history.less | 4 ++-- resources/js/group-history/events.tsx | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/resources/css/bem/group-history.less b/resources/css/bem/group-history.less index 73c26a514b7..3cc64fdae70 100644 --- a/resources/css/bem/group-history.less +++ b/resources/css/bem/group-history.less @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. .group-history { - &__events { + &--events { display: flex; flex-direction: column; gap: 10px; } - &__none { + &--none { font-size: @font-size--title-small-3; margin: 0; text-align: center; diff --git a/resources/js/group-history/events.tsx b/resources/js/group-history/events.tsx index 86aa5524594..eb1bca0b00c 100644 --- a/resources/js/group-history/events.tsx +++ b/resources/js/group-history/events.tsx @@ -4,9 +4,12 @@ import UserGroupEventJson from 'interfaces/user-group-event-json'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { classWithModifiers } from 'utils/css'; import { trans } from 'utils/lang'; import Event from './event'; +const bn = 'group-history'; + interface Props { events: UserGroupEventJson[]; } @@ -15,13 +18,13 @@ interface Props { export default class Events extends React.Component { render() { return this.props.events.length > 0 ? ( -
+
{this.props.events.map((event) => ( ))}
) : ( -

+

{trans('group_history.none')}

); From 01609229ecead7f215a63a5fa73d4f985de57db7 Mon Sep 17 00:00:00 2001 From: clayton Date: Mon, 20 May 2024 23:31:46 -0700 Subject: [PATCH 11/27] Readonly lint --- resources/js/group-history/main.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/group-history/main.tsx b/resources/js/group-history/main.tsx index af6a7ed4420..a149c6291f2 100644 --- a/resources/js/group-history/main.tsx +++ b/resources/js/group-history/main.tsx @@ -21,10 +21,10 @@ import SearchForm from './search-form'; export default class Main extends React.Component { @observable private currentParams: GroupHistoryJson['params']; @observable private cursorString: string | null; - private disposeQueryStringUpdater: IReactionDisposer; + private readonly disposeQueryStringUpdater: IReactionDisposer; @observable private events: UserGroupEventJson[]; @observable private loading?: 'more' | 'new'; - @observable private newParams: GroupHistoryJson['params']; + @observable private readonly newParams: GroupHistoryJson['params']; private xhr?: JQuery.jqXHR; constructor(props: GroupHistoryJson) { From 0cea8e56cbf8a7886d0a9080a36fda899645e3f2 Mon Sep 17 00:00:00 2001 From: clayton Date: Mon, 20 May 2024 23:47:45 -0700 Subject: [PATCH 12/27] Set css vars at group-history-event block --- resources/css/bem/group-history-event.less | 52 +++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/resources/css/bem/group-history-event.less b/resources/css/bem/group-history-event.less index 0fb5420754c..f466a99e3b9 100644 --- a/resources/css/bem/group-history-event.less +++ b/resources/css/bem/group-history-event.less @@ -2,31 +2,37 @@ // See the LICENCE file in the repository root for full licence text. .group-history-event { - @_top: group-history-event; - + --message-weight: normal; align-items: center; display: flex; font-size: @font-size--title-small; gap: 15px; - &__icon { - @icons: { - group-add: users; - group-remove: users-slash; - group-rename: users-cog; - user-add: user-plus; - user-add-playmodes: user-tag; - user-remove: user-minus; - user-remove-playmodes: user-tag; - user-set-default: user-cog; - }; - each(@icons, { - .@{_top}--@{key} & { - @icon-var: 'fa-var-@{value}'; - --icon: @@icon-var; - } - }); + @bold-events: group-add, group-remove, group-rename; + each(@bold-events, { + &--@{value} { + --message-weight: bold; + } + }); + @icons: { + group-add: users; + group-remove: users-slash; + group-rename: users-cog; + user-add: user-plus; + user-add-playmodes: user-tag; + user-remove: user-minus; + user-remove-playmodes: user-tag; + user-set-default: user-cog; + }; + each(@icons, { + &--@{key} { + @icon-var: 'fa-var-@{value}'; + --icon: @@icon-var; + } + }); + + &__icon { .fas(); background-color: var(--group-colour, hsl(var(--hsl-b1))); border-radius: 10000px; @@ -53,13 +59,7 @@ } &__message { - @bold-events: group-add, group-remove, group-rename; - each(@bold-events, { - .@{_top}--@{value} & { - font-weight: bold; - } - }); - flex-grow: 1; + font-weight: var(--message-weight); } } From 0738dba382730a773d73ccda9e541a3fa5df71a1 Mon Sep 17 00:00:00 2001 From: clayton Date: Tue, 28 May 2024 02:41:12 -0700 Subject: [PATCH 13/27] Fix `for` on labels --- resources/js/group-history/search-form.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/js/group-history/search-form.tsx b/resources/js/group-history/search-form.tsx index 279c1d9fa65..138b32e31ef 100644 --- a/resources/js/group-history/search-form.tsx +++ b/resources/js/group-history/search-form.tsx @@ -38,10 +38,11 @@ export default class SearchForm extends React.Component { return (
- +
- + - + - + Date: Tue, 28 May 2024 02:41:12 -0700 Subject: [PATCH 14/27] Use form-select for group selector --- resources/css/bem/form-select.less | 13 +++++++++++++ resources/css/bem/group-history-search-form.less | 15 --------------- resources/js/group-history/search-form.tsx | 4 ++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/resources/css/bem/form-select.less b/resources/css/bem/form-select.less index 9707f24f967..0071eb93382 100644 --- a/resources/css/bem/form-select.less +++ b/resources/css/bem/form-select.less @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. .form-select { + @_top: form-select; + .default-border-radius(); background-color: @osu-colour-b6; color: @osu-colour-c1; @@ -40,6 +42,12 @@ flex: 1; } + &--group-history { + background-color: inherit; + font-size: @font-size--title-small-3; + margin: -5px -10px -5px -5px; + } + &--simple-form { background-color: @osu-colour-b4; color: @osu-colour-c1; @@ -52,5 +60,10 @@ background-color: inherit; border-radius: inherit; max-width: 100%; + + .@{_top}--group-history & { + text-overflow: ellipsis; + width: 100%; + } } } diff --git a/resources/css/bem/group-history-search-form.less b/resources/css/bem/group-history-search-form.less index 03fa0f64a6f..cdb33d57ac2 100644 --- a/resources/css/bem/group-history-search-form.less +++ b/resources/css/bem/group-history-search-form.less @@ -45,19 +45,4 @@ color: var(--label-colour); padding-bottom: 5px; } - - &__select-container { - position: relative; - - &::after { - .center-content(); - .fas(); - content: @fa-var-chevron-down; - height: 100%; - padding-left: 10px; - pointer-events: none; - position: absolute; - right: 5px; - } - } } diff --git a/resources/js/group-history/search-form.tsx b/resources/js/group-history/search-form.tsx index 138b32e31ef..a4d95e9bd5a 100644 --- a/resources/js/group-history/search-form.tsx +++ b/resources/js/group-history/search-form.tsx @@ -39,9 +39,9 @@ export default class SearchForm extends React.Component {
-
+
{
- + Date: Mon, 17 Jun 2024 03:59:37 -0700 Subject: [PATCH 22/27] Fixed with icons --- resources/css/bem/group-history-event.less | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/css/bem/group-history-event.less b/resources/css/bem/group-history-event.less index b146475db9c..deba1485ab2 100644 --- a/resources/css/bem/group-history-event.less +++ b/resources/css/bem/group-history-event.less @@ -32,13 +32,15 @@ }); &__icon { - .fas(); background-color: var(--group-colour, hsl(var(--hsl-b1))); border-radius: 10000px; color: hsl(var(--hsl-b6)); + line-height: 1; padding: 3px 6px; &::before { + .fas(); + .fa-fw(); content: var(--icon); } } From 5c9e70f244efdec325be25f388774fb0093993ba Mon Sep 17 00:00:00 2001 From: clayton Date: Mon, 17 Jun 2024 03:59:38 -0700 Subject: [PATCH 23/27] Refactor current param updating & button behavior Each way to update the current params (new search or "show more") now keeps its own state so that they can each abort the ongoing request and still function as expected. The disabled state of the buttons now reflect this Additionally the current params are updated immediately on starting a search, instead of when the request completes --- resources/js/group-history/main.tsx | 82 +++++++++++++--------- resources/js/group-history/search-form.tsx | 15 ++-- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/resources/js/group-history/main.tsx b/resources/js/group-history/main.tsx index a149c6291f2..312c7abb88e 100644 --- a/resources/js/group-history/main.tsx +++ b/resources/js/group-history/main.tsx @@ -6,7 +6,8 @@ import ShowMoreLink from 'components/show-more-link'; import StringWithComponent from 'components/string-with-component'; import UserGroupEventJson from 'interfaces/user-group-event-json'; import { route } from 'laroute'; -import { IReactionDisposer, action, autorun, makeObservable, observable } from 'mobx'; +import { omit } from 'lodash'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { onErrorWithCallback } from 'utils/ajax'; @@ -17,35 +18,32 @@ import groupStore from './group-store'; import GroupHistoryJson from './json'; import SearchForm from './search-form'; +type MoreParams = GroupHistoryJson['params'] & { cursor_string: string }; + +export const formParamKeys = ['group', 'max_date', 'min_date', 'user'] as const; + @observer export default class Main extends React.Component { @observable private currentParams: GroupHistoryJson['params']; - @observable private cursorString: string | null; - private readonly disposeQueryStringUpdater: IReactionDisposer; @observable private events: UserGroupEventJson[]; - @observable private loading?: 'more' | 'new'; + @observable private loading: 'more' | 'new' | false = false; + @observable private moreParams!: MoreParams | null; @observable private readonly newParams: GroupHistoryJson['params']; private xhr?: JQuery.jqXHR; + @computed + private get newParamsSameAsCurrent() { + return formParamKeys.every((key) => this.newParams[key] === this.currentParams[key]); + } + constructor(props: GroupHistoryJson) { super(props); groupStore.update(props.groups); this.currentParams = props.params; - this.cursorString = props.cursor_string; this.events = props.events; this.newParams = { ...this.currentParams }; - - // Update the query string of the URL whenever the current params are - // modified. Remove "sort" from the query if it's set to the default - this.disposeQueryStringUpdater = autorun(() => { - const params = { - ...this.currentParams, - sort: this.currentParams.sort === 'id_desc' ? null : this.currentParams.sort, - }; - - history.replaceState(history.state, '', updateQueryString(null, params)); - }); + this.setMoreParamsFromJson(props); // If the "group" param doesn't match any group we can show as a select // option, set it to null in the new params. This prevents the new params @@ -58,7 +56,6 @@ export default class Main extends React.Component { } componentWillUnmount() { - this.disposeQueryStringUpdater(); this.xhr?.abort(); } @@ -68,17 +65,17 @@ export default class Main extends React.Component {
@@ -100,38 +97,57 @@ export default class Main extends React.Component { } @action - private loadEvents(params: GroupHistoryJson['params'], cursorString?: string) { + private loadEvents(params: GroupHistoryJson['params'] | MoreParams) { this.xhr?.abort(); - this.loading = cursorString == null ? 'new' : 'more'; + this.currentParams = omit(params, 'cursor_string'); + this.loading = 'cursor_string' in params ? 'more' : 'new'; this.xhr = $.ajax( route('group-history.index'), { - data: { ...params, cursor_string: cursorString }, + data: params, dataType: 'json', method: 'GET', }, ) .done(action((response: GroupHistoryJson) => { groupStore.update(response.groups); - this.currentParams = response.params; - this.cursorString = response.cursor_string; + this.setMoreParamsFromJson(response); - if (cursorString == null) { + if (this.loading === 'new') { this.events = response.events; } else { this.events.push(...response.events); } })) - .fail(onErrorWithCallback(() => this.loadEvents(params, cursorString))) - .always(action(() => this.loading = undefined)); + .fail(onErrorWithCallback(() => this.loadEvents(params))) + .always(action(() => this.loading = false)); } - private readonly onSearch = () => this.loadEvents(this.newParams); + private readonly onMore = () => { + if (this.moreParams != null) { + this.loadEvents(this.moreParams); + } + }; - private readonly onShowMore = () => { - if (this.cursorString != null) { - this.loadEvents(this.currentParams, this.cursorString); + private readonly onNewSearch = () => { + if (this.newParamsSameAsCurrent) { + return; } + + // Update the query string of the URL when starting a new search. Remove + // "sort" from the query if it's set to the default + history.replaceState(history.state, '', updateQueryString(null, { + ...this.newParams, + sort: this.newParams.sort === 'id_desc' ? null : this.newParams.sort, + })); + + this.loadEvents(this.newParams); }; + + private setMoreParamsFromJson(json: GroupHistoryJson) { + this.moreParams = json.cursor_string == null + ? null + : { ...json.params, cursor_string: json.cursor_string }; + } } diff --git a/resources/js/group-history/search-form.tsx b/resources/js/group-history/search-form.tsx index c6b0bce4ce9..4116a3dbd65 100644 --- a/resources/js/group-history/search-form.tsx +++ b/resources/js/group-history/search-form.tsx @@ -9,12 +9,12 @@ import * as React from 'react'; import { trans } from 'utils/lang'; import groupStore from './group-store'; import GroupHistoryJson from './json'; +import { formParamKeys } from './main'; const bn = 'group-history-search-form'; -const formParamKeys = ['group', 'max_date', 'min_date', 'user'] as const; interface Props { - currentParams: GroupHistoryJson['params']; + disabled: boolean; loading: boolean; newParams: GroupHistoryJson['params']; onSearch: () => void; @@ -28,12 +28,7 @@ export default class SearchForm extends React.Component { } render() { - const newParamsEmpty = formParamKeys.every( - (key) => this.props.newParams[key] == null, - ); - const newParamsSame = formParamKeys.every( - (key) => this.props.newParams[key] === this.props.currentParams[key], - ); + const newParamsEmpty = formParamKeys.every((key) => this.props.newParams[key] == null); return ( @@ -98,9 +93,9 @@ export default class SearchForm extends React.Component { text={trans('common.buttons.reset')} /> Date: Mon, 17 Jun 2024 03:59:38 -0700 Subject: [PATCH 24/27] Fix state when navigating back --- resources/js/entrypoints/group-history.tsx | 5 +--- resources/js/group-history/main.tsx | 32 ++++++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/resources/js/entrypoints/group-history.tsx b/resources/js/entrypoints/group-history.tsx index e94d0adac6c..859dbe6a4d6 100644 --- a/resources/js/entrypoints/group-history.tsx +++ b/resources/js/entrypoints/group-history.tsx @@ -4,8 +4,5 @@ import Main from 'group-history/main'; import core from 'osu-core-singleton'; import * as React from 'react'; -import { parseJson } from 'utils/json'; -core.reactTurbolinks.register('group-history', () => ( -
-)); +core.reactTurbolinks.register('group-history', () =>
); diff --git a/resources/js/group-history/main.tsx b/resources/js/group-history/main.tsx index 312c7abb88e..b7bcfbdd88f 100644 --- a/resources/js/group-history/main.tsx +++ b/resources/js/group-history/main.tsx @@ -7,10 +7,11 @@ import StringWithComponent from 'components/string-with-component'; import UserGroupEventJson from 'interfaces/user-group-event-json'; import { route } from 'laroute'; import { omit } from 'lodash'; -import { action, computed, makeObservable, observable } from 'mobx'; -import { observer } from 'mobx-react'; +import { action, autorun, computed, makeObservable, observable } from 'mobx'; +import { disposeOnUnmount, observer } from 'mobx-react'; import * as React from 'react'; import { onErrorWithCallback } from 'utils/ajax'; +import { parseJson, storeJson } from 'utils/json'; import { trans } from 'utils/lang'; import { updateQueryString, wikiUrl } from 'utils/url'; import Events from './events'; @@ -21,9 +22,10 @@ import SearchForm from './search-form'; type MoreParams = GroupHistoryJson['params'] & { cursor_string: string }; export const formParamKeys = ['group', 'max_date', 'min_date', 'user'] as const; +const jsonId = 'json-group-history'; @observer -export default class Main extends React.Component { +export default class Main extends React.Component { @observable private currentParams: GroupHistoryJson['params']; @observable private events: UserGroupEventJson[]; @observable private loading: 'more' | 'new' | false = false; @@ -36,23 +38,31 @@ export default class Main extends React.Component { return formParamKeys.every((key) => this.newParams[key] === this.currentParams[key]); } - constructor(props: GroupHistoryJson) { + constructor(props: Record) { super(props); + makeObservable(this); + + const json = parseJson(jsonId); - groupStore.update(props.groups); - this.currentParams = props.params; - this.events = props.events; + groupStore.update(json.groups); + this.currentParams = json.params; + this.events = json.events; this.newParams = { ...this.currentParams }; - this.setMoreParamsFromJson(props); + this.setMoreParamsFromJson(json); // If the "group" param doesn't match any group we can show as a select // option, set it to null in the new params. This prevents the new params // from initially being out of sync with the displayed form controls - if (props.params.group != null && !groupStore.byIdentifier.has(props.params.group)) { + if (json.params.group != null && !groupStore.byIdentifier.has(json.params.group)) { this.newParams.group = null; } - makeObservable(this); + disposeOnUnmount(this, autorun(() => storeJson(jsonId, { + cursor_string: this.moreParams?.cursor_string ?? null, + events: this.events, + groups: groupStore.all, + params: this.currentParams, + }))); } componentWillUnmount() { @@ -137,7 +147,7 @@ export default class Main extends React.Component { // Update the query string of the URL when starting a new search. Remove // "sort" from the query if it's set to the default - history.replaceState(history.state, '', updateQueryString(null, { + Turbolinks.controller.replaceHistory(updateQueryString(null, { ...this.newParams, sort: this.newParams.sort === 'id_desc' ? null : this.newParams.sort, })); From 7ec8a6c7935bcc91b34d366a649704b92a14b740 Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 20 Jun 2024 04:08:37 -0700 Subject: [PATCH 25/27] Unneeded for/id for label/input --- resources/js/group-history/search-form.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/resources/js/group-history/search-form.tsx b/resources/js/group-history/search-form.tsx index 4116a3dbd65..b28f41e674f 100644 --- a/resources/js/group-history/search-form.tsx +++ b/resources/js/group-history/search-form.tsx @@ -33,11 +33,10 @@ export default class SearchForm extends React.Component { return (
- +
- + - + - + Date: Thu, 20 Jun 2024 04:12:17 -0700 Subject: [PATCH 26/27] Merge group history event type styling --- resources/css/bem/group-history-event.less | 27 ++++++++-------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/resources/css/bem/group-history-event.less b/resources/css/bem/group-history-event.less index deba1485ab2..67c89b98968 100644 --- a/resources/css/bem/group-history-event.less +++ b/resources/css/bem/group-history-event.less @@ -2,32 +2,25 @@ // See the LICENCE file in the repository root for full licence text. .group-history-event { - --message-weight: normal; align-items: center; display: flex; font-size: @font-size--title-small; gap: 15px; - @bold-events: group_add, group_remove, group_rename; - each(@bold-events, { - &--@{value} { - --message-weight: bold; - } - }); - @icons: { - group_add: @fa-var-users; - group_remove: @fa-var-users-slash; - group_rename: @fa-var-users-cog; - user_add: @fa-var-user-plus; - user_add_playmodes: @fa-var-user-tag; - user_remove: @fa-var-user-minus; - user_remove_playmodes: @fa-var-user-tag; - user_set_default: @fa-var-user-cog; + group_add: @fa-var-users, bold; + group_remove: @fa-var-users-slash, bold; + group_rename: @fa-var-users-cog, bold; + user_add: @fa-var-user-plus, normal; + user_add_playmodes: @fa-var-user-tag, normal; + user_remove: @fa-var-user-minus, normal; + user_remove_playmodes: @fa-var-user-tag, normal; + user_set_default: @fa-var-user-cog, normal; }; each(@icons, { &--@{key} { - --icon: @value; + --icon: extract(@value, 1); + --message-weight: extract(@value, 2); } }); From 87573fe8eb3be9a6c1295c9eea53cfb434c7a748 Mon Sep 17 00:00:00 2001 From: clayton Date: Thu, 20 Jun 2024 04:13:47 -0700 Subject: [PATCH 27/27] Better name --- resources/css/bem/group-history-event.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/css/bem/group-history-event.less b/resources/css/bem/group-history-event.less index 67c89b98968..c0cba6348d5 100644 --- a/resources/css/bem/group-history-event.less +++ b/resources/css/bem/group-history-event.less @@ -7,7 +7,7 @@ font-size: @font-size--title-small; gap: 15px; - @icons: { + @types: { group_add: @fa-var-users, bold; group_remove: @fa-var-users-slash, bold; group_rename: @fa-var-users-cog, bold; @@ -17,7 +17,7 @@ user_remove_playmodes: @fa-var-user-tag, normal; user_set_default: @fa-var-user-cog, normal; }; - each(@icons, { + each(@types, { &--@{key} { --icon: extract(@value, 1); --message-weight: extract(@value, 2);