32 private const SUBPAGE_EDIT =
'edit';
33 private const SUBPAGE_DELETE =
'delete';
34 private const PARAM_ID =
'wll_id';
35 private const PARAM_IDS =
'wll_ids';
36 private const PARAM_NAME =
'wll_name';
37 private const SESSION_SUCCESS_TYPE =
'watchlistLabelsSuccessType';
38 private const SESSION_SUCCESS_COUNT =
'watchlistLabelsSuccessCount';
39 private const SESSION_SUCCESS_LABEL_NAME =
'watchlistLabelsSuccessLabelName';
40 private const SESSION_SUCCESS_OLD_LABEL_NAME =
'watchlistLabelsSuccessOldLabelName';
41 private const SUCCESS_CREATE =
'create';
42 private const SUCCESS_EDIT =
'edit';
43 private const SUCCESS_DELETE =
'delete';
45 use WatchlistSpecialPage;
52 $name =
'WatchlistLabels',
53 $restriction =
'viewmywatchlist',
55 parent::__construct( $name, $restriction,
false );
66 $right = $subPage === self::SUBPAGE_EDIT ?
'editmywatchlist' :
'viewmywatchlist';
67 if ( !$user->isRegistered()
68 || ( $user->isTemp() && !$user->isAllowed( $right ) )
78 $output->setPageTitleMsg( $this->
msg(
'watchlistlabels-title' ) );
79 $output->addModuleStyles(
'mediawiki.codex.messagebox.styles' );
82 $output->addHTML( Html::errorBox( $this->
msg(
'watchlistlabels-not-enabled' )->escaped() ) );
86 $this->outputSubtitle();
88 if ( $subPage === self::SUBPAGE_EDIT ) {
89 $this->showEditForm();
90 } elseif ( $subPage === self::SUBPAGE_DELETE ) {
91 $this->showDeleteConfirmation();
100 private function showEditForm() {
101 $id = $this->
getRequest()->getInt( self::PARAM_ID );
103 self::PARAM_NAME => [
105 'name' => self::PARAM_NAME,
106 'label-message' =>
'watchlistlabels-form-field-name',
107 'validation-callback' => [ $this,
'validateName' ],
108 'filter-callback' => [ $this,
'filterName' ],
114 $this->watchlistLabel = $this->labelStore->loadById( $this->
getUser(), $id );
115 if ( $this->watchlistLabel ) {
116 $descriptor[self::PARAM_NAME][
'default'] = $this->watchlistLabel->getName();
117 $descriptor[self::PARAM_ID] = [
119 'name' => self::PARAM_ID,
120 'default' => $this->watchlistLabel->getId(),
125 $form = HTMLForm::factory(
'codex', $descriptor, $this->
getContext() )
129 ->setHeaderHtml(
Html::element(
'h3', [], $this->
msg(
"watchlistlabels-form-header-$msgSuffix" )->text() ) )
135 ->setSubmitTextMsg(
"watchlistlabels-form-submit-$msgSuffix" )
136 ->setSubmitCallback( [ $this,
'onEditFormSubmit' ] );
151 return $label->getName();
162 $length = strlen( trim( $value ) );
163 if ( $length === 0 ) {
164 return StatusValue::newFatal(
'watchlistlabels-form-name-too-short', $length );
166 if ( $length > 255 ) {
167 return StatusValue::newFatal(
'watchlistlabels-form-name-too-long', $length );
169 $existingLabel = $this->labelStore->loadByName( $this->
getUser(), $value );
170 $thisId = $alldata[self::PARAM_ID] ??
null;
171 if ( $existingLabel && $existingLabel->getId() !== (
int)$thisId ) {
172 return StatusValue::newFatal(
'watchlistlabels-form-name-exists', $existingLabel->getName() );
174 return StatusValue::newGood();
184 if ( !isset( $data[self::PARAM_NAME] ) ) {
185 throw new InvalidArgumentException(
'No name data submitted.' );
187 $successType = self::SUCCESS_CREATE;
189 if ( $this->watchlistLabel && $this->watchlistLabel->getId() ) {
190 $successType = self::SUCCESS_EDIT;
191 $oldName = $this->watchlistLabel->getName();
193 if ( !$this->watchlistLabel ) {
194 $this->watchlistLabel =
new WatchlistLabel( $this->
getUser(), $data[self::PARAM_NAME] );
196 $this->watchlistLabel->setName( $data[self::PARAM_NAME] );
198 $saved = $this->labelStore->save( $this->watchlistLabel );
199 if ( $saved->isOK() ) {
201 $session->set( self::SESSION_SUCCESS_TYPE, $successType );
202 $session->set( self::SESSION_SUCCESS_LABEL_NAME, $this->watchlistLabel->getName() );
203 if ( $successType === self::SUCCESS_EDIT && $oldName !==
null ) {
204 $session->set( self::SESSION_SUCCESS_OLD_LABEL_NAME, $oldName );
208 return Status::cast( $saved );
211 private function showDeleteConfirmation(): void {
212 $ids = $this->
getRequest()->getArray( self::PARAM_IDS ) ?? [];
213 $labels = $this->labelStore->loadAllForUser( $this->
getUser() );
214 $toDelete = array_intersect_key( $labels, array_flip( $ids ) );
215 $labelCounts = $this->labelStore->countItems( array_keys( $labels ) );
217 foreach ( $toDelete as $label ) {
218 $listItems .= Html::rawElement(
'li', [], $this->getDeleteConfirmationListItem( $label, $labelCounts ) );
220 $count = count( $toDelete );
221 $formattedCount = $this->getLanguage()->formatNum( $count );
222 $msg = $this->
msg(
'watchlistlabels-delete-warning', $count, $formattedCount )->text();
224 $list = Html::rawElement(
'ol', [], $listItems );
228 'default' => $warning . $list,
233 'name' => self::PARAM_IDS,
234 'default' => implode(
',', array_keys( $toDelete ) ),
237 $header = $this->
msg(
'watchlistlabels-delete-header', $count )->text();
238 HTMLForm::factory(
'codex', $descriptor, $this->
getContext() )
241 ->setCancelTarget( $this->getPageTitle() )
242 ->setSubmitTextMsg(
'delete' )
243 ->setSubmitDestructive()
244 ->setSubmitCallback( [ $this,
'onDeleteFormSubmit' ] )
248 private function getDeleteConfirmationListItem( WatchlistLabel $label, array $labelCounts ): string {
249 $id = $label->getId();
253 $itemCount = $labelCounts[ $id ];
254 if ( $labelCounts[ $id ] > 0 ) {
255 $labelCountMsg = $this->
msg(
256 'watchlistlabels-delete-count',
257 $this->getLanguage()->formatNum( $itemCount ),
261 $labelCountMsg = $this->
msg(
'watchlistlabels-delete-unused' )->escaped();
263 return Html::element(
'span', [], $label->getName() )
264 . $this->
msg(
'word-separator' )->escaped()
265 . $this->
msg(
'parentheses-start' )->escaped()
266 . Html::rawElement(
'em', [], $labelCountMsg )
267 . $this->
msg(
'parentheses-end' )->escaped();
277 if ( !isset( $data[self::PARAM_IDS] ) ) {
278 throw new InvalidArgumentException(
'No name data submitted.' );
280 $ids = array_map(
'intval', array_filter( explode(
',', $data[self::PARAM_IDS] ) ) );
281 $labels = $this->labelStore->loadAllForUser( $this->getUser() );
282 $deletedLabelNames = [];
283 foreach ( $ids as $id ) {
284 if ( isset( $labels[$id] ) ) {
285 $deletedLabelNames[] = $labels[$id]->getName();
288 if ( $this->labelStore->delete( $this->getUser(), $ids ) ) {
289 $session = $this->getRequest()->getSession();
290 $session->set( self::SESSION_SUCCESS_TYPE, self::SUCCESS_DELETE );
291 $session->set( self::SESSION_SUCCESS_COUNT, count( $ids ) );
292 if ( count( $deletedLabelNames ) === 1 ) {
293 $session->set( self::SESSION_SUCCESS_LABEL_NAME, $deletedLabelNames[0] );
295 $this->getOutput()->redirect( $this->getPageTitle()->getFullUrlForRedirect() );
296 return Status::newGood();
298 return Status::newFatal(
'watchlistlabels-delete-failed' );
304 private function showTable() {
305 $codex =
new Codex();
306 $this->getOutput()->addModules(
'mediawiki.special.watchlistlabels' );
307 $this->getOutput()->addModuleStyles(
'mediawiki.special.watchlistlabels.styles' );
308 $this->showSuccessMessage();
311 $this->getOutput()->addHTML(
312 Html::element(
'h3', [], $this->msg(
'watchlistlabels-table-title' )->text() )
313 . Html::element(
'p', [], $this->msg(
'watchlistlabels-table-description' )->text() ),
318 'href' => $this->getPageTitle( self::SUBPAGE_EDIT )->getLinkURL(),
320 'class' =>
'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled'
321 .
' cdx-button--action-progressive cdx-button--weight-primary'
323 $newIcon = Html::element(
'span', [
324 'class' =>
'cdx-button__icon mw-specialwatchlistlabels-icon--add',
325 'aria-hidden' =>
'true',
327 $createLabel = $newIcon .
' ' . $this->msg(
'watchlistlabels-table-new-link' )->escaped();
328 $createButton = Html::rawElement(
'a', $params, $createLabel );
329 $deleteButton = $codex->button()
330 ->setAttributes( [
'class' =>
'mw-specialwatchlistlabels-delete-button' ] )
331 ->setLabel( $this->msg(
'watchlistlabels-table-delete-link' )->text() )
332 ->setIconClass(
'mw-specialwatchlistlabels-icon--trash' )
333 ->setType(
'submit' )
334 ->setAction(
'destructive' )
340 $labels = $this->labelStore->loadAllForUser( $this->getUser() );
341 $labelCounts = $this->labelStore->countItems( array_keys( $labels ) );
342 $editIcon = Html::element(
'span', [
'class' =>
'cdx-button__icon mw-specialwatchlistlabels-icon--edit' ] );
343 foreach ( $labels as $label ) {
344 $id = $label->getId();
348 $url = $this->getPageTitle( self::SUBPAGE_EDIT )->getLocalURL( [ self::PARAM_ID => $id ] );
352 'class' =>
'cdx-button cdx-button--fake-button cdx-button--fake-button--enabled'
353 .
' cdx-button--weight-quiet cdx-button--icon-only cdx-button--size-small',
354 'title' => $this->msg(
'watchlistlabels-table-edit' )->text(),
356 $checkboxId = self::PARAM_IDS .
'_' . $id;
357 $labelVal = Html::element(
'bdi', [], $label->getName() );
360 'select' => $this->getCheckbox( $checkboxId, (
string)$id ),
361 'name' => Html::rawElement(
'label', [
'for' => $checkboxId ], $labelVal ),
362 'name-sort' => mb_strtolower( $label->getName() ),
363 'count' => $this->getLanguage()->formatNum( $labelCounts[ $id ] ),
364 'count-sort' => $labelCounts[ $id ],
365 'edit' => Html::rawElement(
'a', $params, $editIcon ),
372 $sortCol = $this->getRequest()->getText(
'sort',
'count' );
373 $sortDir = $this->getRequest()->getBool(
'asc' ) ? TableBuilder::SORT_ASCENDING : TableBuilder::SORT_DESCENDING;
374 $sortColName = $sortCol .
'-sort';
377 static function ( $a, $b ) use ( $sortDir, $sortColName ) {
378 if ( !isset( $a[$sortColName] )
379 || !isset( $b[$sortColName] )
380 || $a[$sortColName] === $b[$sortColName]
384 return $sortDir === TableBuilder::SORT_ASCENDING
385 ? $a[$sortColName] <=> $b[$sortColName]
386 : $b[$sortColName] <=> $a[$sortColName];
391 $table = $codex->table()
392 ->setCurrentSortColumn( $sortCol )
393 ->setCurrentSortDirection( $sortDir )
394 ->setAttributes( [
'class' =>
'mw-specialwatchlistlabels-table' ] )
395 ->setCaption( $this->
msg(
'watchlistlabels-table-header' )->text() )
396 ->setHeaderContent(
"$createButton $deleteButton" )
404 'label' => $this->
msg(
'watchlistlabels-table-col-name' )->escaped(),
409 'label' => $this->
msg(
'watchlistlabels-table-col-count' )->escaped(),
414 'label' => $this->
msg(
'watchlistlabels-table-col-actions' )->escaped(),
418 ->setPaginate(
false )
420 $deleteUrl = $this->getPageTitle( self::SUBPAGE_DELETE )->getLocalURL();
421 $form = Html::rawElement(
'form', [
'action' => $deleteUrl ], $table->getHtml() );
429 private function showSuccessMessage(): void {
431 $successType = $session->get( self::SESSION_SUCCESS_TYPE );
432 if ( !$successType ) {
435 $successCount = $session->get( self::SESSION_SUCCESS_COUNT );
436 $successLabelName = $session->get( self::SESSION_SUCCESS_LABEL_NAME );
437 $successOldLabelName = $session->get( self::SESSION_SUCCESS_OLD_LABEL_NAME );
440 $session->remove( self::SESSION_SUCCESS_TYPE );
441 $session->remove( self::SESSION_SUCCESS_COUNT );
442 $session->remove( self::SESSION_SUCCESS_LABEL_NAME );
443 $session->remove( self::SESSION_SUCCESS_OLD_LABEL_NAME );
446 switch ( $successType ) {
447 case self::SUCCESS_CREATE:
448 $message = $this->
msg(
'watchlistlabels-success-created', $successLabelName )->escaped();
450 case self::SUCCESS_EDIT:
451 if ( !$successOldLabelName || !$successLabelName ) {
454 $message = $this->
msg(
455 'watchlistlabels-success-edited',
456 $successOldLabelName,
460 case self::SUCCESS_DELETE:
461 $count = max( 0, (
int)$successCount );
462 if ( $count === 0 ) {
465 $message = $this->
msg(
'watchlistlabels-success-deleted', $successLabelName, $count )->escaped();
469 if ( $message !==
null ) {
470 $this->
getOutput()->addHTML( Html::successBox( $message ) );
482 private function getCheckbox(
string $id,
string $value ): string {
483 $checkbox = Html::check(
484 self::PARAM_IDS .
'[]',
486 [
'value' => $value,
'class' =>
'cdx-checkbox__input',
'id' => $id ]
488 $checkboxIcon = Html::element(
'span', [
'class' =>
'cdx-checkbox__icon' ] );
489 $checkboxWrapper = Html::rawElement(
491 [
'class' =>
'cdx-checkbox__wrapper' ],
492 $checkbox . $checkboxIcon
494 return Html::rawElement(
'div', [
'class' =>
'cdx-checkbox' ], $checkboxWrapper );