<commit_before>// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json';
import GenreJson from 'interfaces/genre-json';
import LanguageJson from 'interfaces/language-json';
import { route } from 'laroute';
import * as React from 'react';
import { onError } from 'utils/ajax';
import { parseJson } from 'utils/json';
import { getInt } from 'utils/math';

interface Props {
  beatmapset: BeatmapsetJsonForShow;
  onClose: () => void;
}

interface State {
  genreId: number;
  isBusy: boolean;
  languageId: number;
  nsfw: boolean;
  offset: string;
}

export default class MetadataEditor extends React.PureComponent<Props, State> {
  private genres = parseJson<GenreJson[]>('json-genres');
  private languages = parseJson<LanguageJson[]>('json-languages');

  get canEditOffset() {
    return this.props.beatmapset.current_user_attributes.can_edit_offset;
  }

  constructor(props: Props) {
    super(props);

    this.state = {
      genreId: props.beatmapset.genre.id ?? 0,
      isBusy: false,
      languageId: props.beatmapset.language.id ?? 0,
      nsfw: props.beatmapset.nsfw ?? false,
      offset: props.beatmapset.offset.toString(),
    };
  }

  render() {
    return (
      <form className='simple-form simple-form--modal'>
        <label className='simple-form__row'>
          <div className='simple-form__label'>
            {osu.trans('beatmapsets.show.info.language')}
          </div>

          <div className='form-select form-select--full'>
            <select
              className='form-select__input'
              name='beatmapset[language_id]'
              onChange={this.setLanguageId}
              value={this.state.languageId}
            >
              {this.languages.map((language) => (
                language.id === null ? null :
                  <option key={language.id} value={language.id}>
                    {language.name}
                  </option>
              ))}
            </select>
          </div>
        </label>

        <label className='simple-form__row'>
          <div className='simple-form__label'>
            {osu.trans('beatmapsets.show.info.genre')}
          </div>

          <div className='form-select form-select--full'>
            <select
              className='form-select__input'
              name='beatmapset[genre_id]'
              onChange={this.setGenreId}
              value={this.state.genreId}
            >
              {this.genres.map((genre) => (
                genre.id === null ? null :
                  <option key={genre.id} value={genre.id}>
                    {genre.name}
                  </option>
              ))}
            </select>
          </div>
        </label>

        {this.canEditOffset &&
          <label className='simple-form__row'>
            <div className='simple-form__label'>
              {osu.trans('beatmapsets.show.info.offset')}
            </div>

            <input
              className='simple-form__input'
              maxLength={6}
              name='beatmapset[offset]'
              onChange={this.setOffset}
              type='text'
              value={this.state.offset}
            />
          </label>
        }

        <div className='simple-form__row'>
          <div className='simple-form__label'>
            {osu.trans('beatmapsets.show.info.nsfw')}
          </div>

          <label className='osu-switch-v2'>
            <input
              checked={this.state.nsfw}
              className='osu-switch-v2__input'
              name='beatmapset[nsfw]'
              onChange={this.setNsfw}
              type='checkbox'
            />
            <span className='osu-switch-v2__content' />
          </label>
        </div>

        <div className='simple-form__row simple-form__row--no-label'>
          <div className='simple-form__buttons'>
            <div className='simple-form__button'>
              <button
                className='btn-osu-big btn-osu-big--rounded-thin'
                disabled={this.state.isBusy}
                onClick={this.save}
                type='button'
              >
                {osu.trans('common.buttons.save')}
              </button>
            </div>

            <div className='simple-form__button'>
              <button
                className='btn-osu-big btn-osu-big--rounded-thin'
                disabled={this.state.isBusy}
                onClick={this.props.onClose}
                type='button'
              >
                {osu.trans('common.buttons.cancel')}
              </button>
            </div>
          </div>
        </div>
      </form>
    );
  }

  private save = () => {
    this.setState({ isBusy: true });

    $.ajax(route('beatmapsets.update', { beatmapset: this.props.beatmapset.id }), {
      data: { beatmapset: {
        genre_id: this.state.genreId,
        language_id: this.state.languageId,
        nsfw: this.state.nsfw,
        offset: this.canEditOffset ? getInt(this.state.offset) : undefined,
      } },
      method: 'PATCH',
    }).done((beatmapset: BeatmapsetJsonForShow) => $.publish('beatmapset:set', { beatmapset }))
      .fail(onError)
      .always(() => this.setState({ isBusy: false }))
      .done(this.props.onClose);
  };

  private setGenreId = (e: React.ChangeEvent<HTMLSelectElement>) => {
    this.setState({ genreId: parseInt(e.currentTarget.value, 10) });
  };

  private setLanguageId = (e: React.ChangeEvent<HTMLSelectElement>) => {
    this.setState({ languageId: parseInt(e.currentTarget.value, 10) });
  };

  private setNsfw = (e: React.ChangeEvent<HTMLInputElement>) => {
    this.setState({ nsfw: e.currentTarget.checked });
  };

  private setOffset = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.currentTarget.value;

    if (/^-?\d*$/.test(value)) {
      this.setState({ offset: value });
    }
  };
}<commit_msg>Mobx metadata editor component<commit_after>// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import { BeatmapsetJsonForShow } from 'interfaces/beatmapset-extended-json';
import GenreJson from 'interfaces/genre-json';
import LanguageJson from 'interfaces/language-json';
import { route } from 'laroute';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { onError } from 'utils/ajax';
import { parseJson } from 'utils/json';
import { getInt } from 'utils/math';

interface Props {
  beatmapset: BeatmapsetJsonForShow;
  onClose: () => void;
}

let genresCache: GenreJson[];
function genres() {
  return genresCache ??= parseJson('json-genres');
}

let languagesCache: LanguageJson[];
function languages() {
  return languagesCache ??= parseJson('json-languages');
}

@observer
export default class MetadataEditor extends React.Component<Props> {
  @observable private genreId: number;
  @observable private languageId: number;
  @observable private nsfw: boolean;
  @observable private offset: string;
  @observable private xhr: JQuery.jqXHR<BeatmapsetJsonForShow> | null = null;

  private get canEditOffset() {
    return this.props.beatmapset.current_user_attributes.can_edit_offset;
  }

  constructor(props: Props) {
    super(props);

    this.genreId = this.props.beatmapset.genre.id ?? 0;
    this.languageId = this.props.beatmapset.language.id ?? 0;
    this.nsfw = this.props.beatmapset.nsfw ?? false;
    this.offset = this.props.beatmapset.offset.toString();

    makeObservable(this);
  }

  componentWillUnmount() {
    this.xhr?.abort();
  }

  render() {
    return (
      <form className='simple-form simple-form--modal'>
        <label className='simple-form__row'>
          <div className='simple-form__label'>
            {osu.trans('beatmapsets.show.info.language')}
          </div>

          <div className='form-select form-select--full'>
            <select
              className='form-select__input'
              name='beatmapset[language_id]'
              onChange={this.setLanguageId}
              value={this.languageId}
            >
              {languages().map((language) => (
                language.id === null ? null :
                  <option key={language.id} value={language.id}>
                    {language.name}
                  </option>
              ))}
            </select>
          </div>
        </label>

        <label className='simple-form__row'>
          <div className='simple-form__label'>
            {osu.trans('beatmapsets.show.info.genre')}
          </div>

          <div className='form-select form-select--full'>
            <select
              className='form-select__input'
              name='beatmapset[genre_id]'
              onChange={this.setGenreId}
              value={this.genreId}
            >
              {genres().map((genre) => (
                genre.id === null ? null :
                  <option key={genre.id} value={genre.id}>
                    {genre.name}
                  </option>
              ))}
            </select>
          </div>
        </label>

        {this.canEditOffset &&
          <label className='simple-form__row'>
            <div className='simple-form__label'>
              {osu.trans('beatmapsets.show.info.offset')}
            </div>

            <input
              className='simple-form__input'
              maxLength={6}
              name='beatmapset[offset]'
              onChange={this.setOffset}
              type='text'
              value={this.offset}
            />
          </label>
        }

        <div className='simple-form__row'>
          <div className='simple-form__label'>
            {osu.trans('beatmapsets.show.info.nsfw')}
          </div>

          <label className='osu-switch-v2'>
            <input
              checked={this.nsfw}
              className='osu-switch-v2__input'
              name='beatmapset[nsfw]'
              onChange={this.setNsfw}
              type='checkbox'
            />
            <span className='osu-switch-v2__content' />
          </label>
        </div>

        <div className='simple-form__row simple-form__row--no-label'>
          <div className='simple-form__buttons'>
            <div className='simple-form__button'>
              <button
                className='btn-osu-big btn-osu-big--rounded-thin'
                disabled={this.xhr != null}
                onClick={this.save}
                type='button'
              >
                {osu.trans('common.buttons.save')}
              </button>
            </div>

            <div className='simple-form__button'>
              <button
                className='btn-osu-big btn-osu-big--rounded-thin'
                disabled={this.xhr != null}
                onClick={this.props.onClose}
                type='button'
              >
                {osu.trans('common.buttons.cancel')}
              </button>
            </div>
          </div>
        </div>
      </form>
    );
  }

  @action
  private readonly save = () => {
    this.xhr = $.ajax(route('beatmapsets.update', { beatmapset: this.props.beatmapset.id }), {
      data: { beatmapset: {
        genre_id: this.genreId,
        language_id: this.languageId,
        nsfw: this.nsfw,
        offset: this.canEditOffset ? getInt(this.offset) : undefined,
      } },
      method: 'PATCH',
    });
    this.xhr.fail(onError).always(action(() => {
      this.xhr = null;
    })).done((beatmapset) => {
      $.publish('beatmapset:set', { beatmapset });
      this.props.onClose();
    });
  };

  @action
  private readonly setGenreId = (e: React.ChangeEvent<HTMLSelectElement>) => {
    this.genreId = parseInt(e.currentTarget.value, 10);
  };

  @action
  private readonly setLanguageId = (e: React.ChangeEvent<HTMLSelectElement>) => {
    this.languageId = parseInt(e.currentTarget.value, 10);
  };

  @action
  private readonly setNsfw = (e: React.ChangeEvent<HTMLInputElement>) => {
    this.nsfw = e.currentTarget.checked;
  };

  @action
  private readonly setOffset = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.currentTarget.value;

    if (/^-?\d*$/.test(value)) {
      this.offset = value;
    }
  };
}
{'fraction_non_alphanumeric': 0.13672598437877445, 'fraction_numerical': 0.0019325227474031727, 'mean_word_length': 1.9627862595419847, 'pattern_counts': {'":': 0, '<': 114, '<?xml version=': 0, '>': 134, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3399225', 'n_tokens_mistral': 3711, 'n_tokens_neox': 3619, 'n_words': 637}