mediawiki.special.block: make submit destructive and use old messages
SpecialBlock calls HTMLForm::setSubmitDestructive(). This patch applies that to the new Vue version of Special:Block. Additionally, since we're not going to enable multiblocks in the initial launch, the submit button message is changed to as it was before. "Save block" is questionable even for multiblocks. It will likely be a while before we iron out the UI for multiblocks, so we might as well remove now-unused 'block-save' message. The "[username] is already blocked" message is now also surfaced, i.e. browsing to [[Special:Block/someblockuser]]. Like the old Special:Block, changes to the block target field won't re-query to see if the user is already blocked (though that would be a fine improvement). However *unlike* the old Special:Block, such changes do make the message disappear. Bug: T373572 Change-Id: Iceaedbb1e3496c52b49a2b96d65445da45261b9f
This commit is contained in:
parent
4e0bc9ad11
commit
de2b3f7a59
11 changed files with 135 additions and 38 deletions
|
|
@ -152,6 +152,10 @@ class SpecialBlock extends FormSpecialPage {
|
|||
|
||||
if ( $this->useCodex ) {
|
||||
$this->getOutput()->addModules( 'mediawiki.special.block.codex' );
|
||||
$this->codexFormData[ 'blockAlreadyBlocked' ] = $this->alreadyBlocked;
|
||||
$this->codexFormData[ 'blockTargetUser' ] = $this->target instanceof UserIdentity ?
|
||||
$this->target->getName() :
|
||||
$this->target ?? null;
|
||||
$this->getOutput()->addJsConfigVars( $this->codexFormData );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2693,7 +2693,6 @@
|
|||
"block-user-label": "Which user do you want to block?",
|
||||
"block-user-description": "A user can be a username, IP address, or an IP range.",
|
||||
"block-user-placeholder": "Start typing a user...",
|
||||
"block-save": "Save block",
|
||||
"block-target": "Username, IP address, or IP range:",
|
||||
"block-target-placeholder": "UserName, 1.1.1.42, or 1.1.1.42/16",
|
||||
"unblockip": "Unblock user",
|
||||
|
|
|
|||
|
|
@ -2956,7 +2956,6 @@
|
|||
"block-user-label": "Label for the input for specifying the user of a block on [[Special:Block]]",
|
||||
"block-user-description": "Description for the input for specifying the user of a block on [[Special:Block]]",
|
||||
"block-user-placeholder": "Placeholder text for the input specifying the user of a block on [[Special:Block]]",
|
||||
"block-save": "Label for submit button on block form [[Special:Block]]",
|
||||
"block-target": "Label for the input for specifying the target of a block on [[Special:Block]]",
|
||||
"block-target-placeholder": "Placeholder text for the input specifying the target of a block on [[Special:Block]]",
|
||||
"unblockip": "Used as title and legend for the form in [[Special:Unblock]].",
|
||||
|
|
|
|||
|
|
@ -2279,20 +2279,25 @@ return [
|
|||
'block-options-description',
|
||||
'block-reason',
|
||||
'block-reason-other',
|
||||
'block-save',
|
||||
'block-user-description',
|
||||
'block-user-label',
|
||||
'block-user-placeholder',
|
||||
'blocklist-type-opt-partial',
|
||||
'blocklist-type-opt-sitewide',
|
||||
'colon-separator',
|
||||
'block-user-label',
|
||||
'block-user-description',
|
||||
'block-user-placeholder',
|
||||
'ipbsubmit',
|
||||
'htmlform-optional-flag',
|
||||
'htmlform-selectorother-other',
|
||||
'ipb-action-create',
|
||||
'ipb-action-move',
|
||||
'ipb-action-upload',
|
||||
'ipb-change-block',
|
||||
'ipb-disableusertalk',
|
||||
'ipb-hardblock',
|
||||
'ipb-needreblock',
|
||||
'ipb-partial-help',
|
||||
'ipb-sitewide-help',
|
||||
'ipbcreateaccount',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
<template>
|
||||
<user-lookup v-model="targetUser"></user-lookup>
|
||||
<cdx-message
|
||||
v-if="targetUser && alreadyBlocked"
|
||||
type="error"
|
||||
inline
|
||||
>
|
||||
{{ $i18n( 'ipb-needreblock', targetUser ).text() }}
|
||||
</cdx-message>
|
||||
<user-lookup
|
||||
v-model="targetUser"
|
||||
@input="alreadyBlocked = false"
|
||||
></user-lookup>
|
||||
<target-active-blocks></target-active-blocks>
|
||||
<target-block-log></target-block-log>
|
||||
<block-type-field
|
||||
|
|
@ -25,18 +35,18 @@
|
|||
></block-details-field>
|
||||
<hr class="mw-block-hr">
|
||||
<cdx-button
|
||||
action="progressive"
|
||||
action="destructive"
|
||||
weight="primary"
|
||||
@click="handleSubmit"
|
||||
class="mw-block-submit"
|
||||
>
|
||||
{{ $i18n( 'block-save' ).text() }}
|
||||
{{ submitBtnMsg }}
|
||||
</cdx-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const { defineComponent, ref } = require( 'vue' );
|
||||
const { CdxButton } = require( '@wikimedia/codex' );
|
||||
const { CdxButton, CdxMessage } = require( '@wikimedia/codex' );
|
||||
const UserLookup = require( './components/UserLookup.vue' );
|
||||
const TargetActiveBlocks = require( './components/TargetActiveBlocks.vue' );
|
||||
const TargetBlockLog = require( './components/TargetBlockLog.vue' );
|
||||
|
|
@ -56,10 +66,11 @@ module.exports = exports = defineComponent( {
|
|||
ExpiryField,
|
||||
ReasonField,
|
||||
BlockDetailsField,
|
||||
CdxButton
|
||||
CdxButton,
|
||||
CdxMessage
|
||||
},
|
||||
setup() {
|
||||
const targetUser = ref( '' );
|
||||
const targetUser = ref( mw.config.get( 'blockTargetUser' ) );
|
||||
const expiry = ref( {} );
|
||||
|
||||
const blockPartialOptions = mw.config.get( 'partialBlockActionOptions' ) ?
|
||||
|
|
@ -206,9 +217,13 @@ module.exports = exports = defineComponent( {
|
|||
} );
|
||||
}
|
||||
|
||||
const alreadyBlocked = mw.config.get( 'blockAlreadyBlocked' );
|
||||
|
||||
return {
|
||||
targetUser,
|
||||
expiry,
|
||||
alreadyBlocked,
|
||||
submitBtnMsg: mw.message( alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit' ).text(),
|
||||
handleSubmit,
|
||||
blockDetailsOptions,
|
||||
blockDetailsSelected,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<cdx-field :is-fieldset="true">
|
||||
<cdx-lookup
|
||||
v-model:selected="wrappedModel"
|
||||
v-model:input-value="wrappedModel"
|
||||
name="wpTarget"
|
||||
:menu-items="menuItems"
|
||||
:start-icon="cdxIconSearch"
|
||||
|
|
@ -36,7 +37,7 @@ module.exports = exports = defineComponent( {
|
|||
],
|
||||
setup( props, { emit } ) {
|
||||
const menuItems = ref( [] );
|
||||
const currentSearchTerm = ref( '' );
|
||||
const currentSearchTerm = ref( props.modelValue || '' );
|
||||
const wrappedModel = useModelWrapper(
|
||||
toRef( props, 'modelValue' ),
|
||||
emit
|
||||
|
|
@ -54,7 +55,6 @@ module.exports = exports = defineComponent( {
|
|||
const api = new mw.Api();
|
||||
|
||||
const params = {
|
||||
origin: '*',
|
||||
action: 'query',
|
||||
format: 'json',
|
||||
formatversion: 2,
|
||||
|
|
@ -96,13 +96,10 @@ module.exports = exports = defineComponent( {
|
|||
}
|
||||
|
||||
// Build an array of menu items.
|
||||
const results = data.allusers.map( ( result ) => ( {
|
||||
menuItems.value = data.allusers.map( ( result ) => ( {
|
||||
label: result.name,
|
||||
value: result.name
|
||||
} ) );
|
||||
|
||||
// Update menuItems.
|
||||
menuItems.value = results;
|
||||
} )
|
||||
.catch( () => {
|
||||
// On error, set results to empty.
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ config.global.directives = {
|
|||
}
|
||||
};
|
||||
|
||||
function ApiMock() {}
|
||||
ApiMock.prototype.get = jest.fn();
|
||||
ApiMock.prototype.post = jest.fn();
|
||||
ApiMock.prototype.postWithEditToken = jest.fn();
|
||||
ApiMock.prototype.postWithToken = jest.fn();
|
||||
|
||||
function RestMock() {}
|
||||
RestMock.prototype.get = jest.fn();
|
||||
|
||||
|
|
@ -41,6 +47,7 @@ TitleMock.prototype.getUrl = jest.fn();
|
|||
|
||||
// Mock the mw global object.
|
||||
const mw = {
|
||||
Api: ApiMock,
|
||||
log: {
|
||||
error: jest.fn(),
|
||||
warn: jest.fn()
|
||||
|
|
|
|||
30
tests/jest/mediawiki.special.block/SpecialBlock.setup.js
Normal file
30
tests/jest/mediawiki.special.block/SpecialBlock.setup.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Mock calls to mw.config.get().
|
||||
* The default implementation correlates to the SpecialBlock::codexFormData property in PHP.
|
||||
*
|
||||
* @param {Object} [config]
|
||||
*/
|
||||
function mockMwConfigGet( config = {} ) {
|
||||
const mockConfig = Object.assign( {
|
||||
blockAlreadyBlocked: false,
|
||||
blockTargetUser: null,
|
||||
blockAllowsEmailBan: true,
|
||||
blockAllowsUTEdit: true,
|
||||
blockAutoblockExpiry: '1 day',
|
||||
blockDefaultExpiry: 'infinite',
|
||||
blockHideUser: true,
|
||||
blockExpiryOptions: {
|
||||
infinite: 'infinite',
|
||||
'Other time:': 'other'
|
||||
},
|
||||
blockReasonOptions: [
|
||||
{ label: 'block-reason-1', value: 'block-reason-1' },
|
||||
{ label: 'block-reason-2', value: 'block-reason-2' }
|
||||
]
|
||||
}, config );
|
||||
mw.config.get.mockImplementation( ( key ) => mockConfig[ key ] );
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mockMwConfigGet
|
||||
};
|
||||
|
|
@ -1,31 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
const { mount } = require( '@vue/test-utils' );
|
||||
|
||||
// Mock calls to mw.config.get.
|
||||
// This correlates to the SpecialBlock::codexFormData property in PHP.
|
||||
const mockConfig = {
|
||||
blockAllowsEmailBan: true,
|
||||
blockAllowsUTEdit: true,
|
||||
blockAutoblockExpiry: '1 day',
|
||||
blockDefaultExpiry: 'infinite',
|
||||
blockHideUser: true,
|
||||
blockExpiryOptions: {
|
||||
infinite: 'infinite',
|
||||
'Other time:': 'other'
|
||||
},
|
||||
blockReasonOptions: [
|
||||
{ label: 'block-reason-1', value: 'block-reason-1' },
|
||||
{ label: 'block-reason-2', value: 'block-reason-2' }
|
||||
]
|
||||
};
|
||||
mw.config.get.mockImplementation( ( key ) => mockConfig[ key ] );
|
||||
|
||||
const { mockMwConfigGet } = require( './SpecialBlock.setup.js' );
|
||||
const SpecialBlock = require( '../../../resources/src/mediawiki.special.block/SpecialBlock.vue' );
|
||||
|
||||
describe( 'SpecialBlock', () => {
|
||||
it( 'should show a submit button with the correct text', () => {
|
||||
const wrapper = mount( SpecialBlock );
|
||||
expect( wrapper.find( 'button.cdx-button' ).text() ).toBe( 'block-save' );
|
||||
it( 'should show a banner and a submit button with text based on if user is already blocked', () => {
|
||||
mockMwConfigGet();
|
||||
let wrapper = mount( SpecialBlock );
|
||||
expect( wrapper.find( 'button.cdx-button' ).text() ).toStrictEqual( 'ipbsubmit' );
|
||||
mockMwConfigGet( {
|
||||
blockAlreadyBlocked: true,
|
||||
blockTargetUser: 'ExampleUser'
|
||||
} );
|
||||
wrapper = mount( SpecialBlock );
|
||||
expect( wrapper.find( '.cdx-message__content' ).text() )
|
||||
.toStrictEqual( 'ipb-needreblock:[ExampleUser]' );
|
||||
expect( wrapper.find( 'button.cdx-button' ).text() ).toStrictEqual( 'ipb-change-block' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
|||
31
tests/jest/mediawiki.special.block/UserLookup.test.js
Normal file
31
tests/jest/mediawiki.special.block/UserLookup.test.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
'use strict';
|
||||
|
||||
const { nextTick } = require( 'vue' );
|
||||
const { mount } = require( '@vue/test-utils' );
|
||||
const UserLookup = require( '../../../resources/src/mediawiki.special.block/components/UserLookup.vue' );
|
||||
|
||||
describe( 'UserLookup', () => {
|
||||
it( 'should update menu items based on the API response', async () => {
|
||||
mw.Api.prototype.get = jest.fn().mockResolvedValue( {
|
||||
query: {
|
||||
allusers: [
|
||||
{ name: 'UserLookup1' },
|
||||
{ name: 'UserLookup2' }
|
||||
]
|
||||
}
|
||||
} );
|
||||
const wrapper = mount( UserLookup, {
|
||||
props: { modelValue: 'UserLookup' }
|
||||
} );
|
||||
const input = wrapper.find( '.cdx-text-input__input' );
|
||||
expect( input.element.value ).toBe( 'UserLookup' );
|
||||
const listBox = wrapper.find( '.cdx-menu__listbox' );
|
||||
// "No results"
|
||||
expect( listBox.element.children ).toHaveLength( 1 );
|
||||
await input.trigger( 'input' );
|
||||
await nextTick();
|
||||
expect( listBox.element.children ).toHaveLength( 2 );
|
||||
expect( listBox.element.children[ 0 ].textContent ).toBe( 'UserLookup1' );
|
||||
expect( listBox.element.children[ 1 ].textContent ).toBe( 'UserLookup2' );
|
||||
} );
|
||||
} );
|
||||
|
|
@ -57,6 +57,7 @@ class SpecialBlockTest extends SpecialPageTestBase {
|
|||
$this->overrideConfigValues( [
|
||||
MainConfigNames::BlockAllowsUTEdit => true,
|
||||
MainConfigNames::EnablePartialActionBlocks => true,
|
||||
MainConfigNames::UseCodexSpecialBlock => false,
|
||||
] );
|
||||
$page = $this->newSpecialPage();
|
||||
$wrappedPage = TestingAccessWrapper::newFromObject( $page );
|
||||
|
|
@ -72,11 +73,30 @@ class SpecialBlockTest extends SpecialPageTestBase {
|
|||
$this->assertArrayHasKey( 'PreviousTarget', $fields );
|
||||
$this->assertArrayHasKey( 'Confirm', $fields );
|
||||
$this->assertArrayHasKey( 'EditingRestriction', $fields );
|
||||
$this->assertArrayNotHasKey( 'options-messages', $fields['EditingRestriction'] );
|
||||
$this->assertArrayNotHasKey( 'option-descriptions-messages', $fields['EditingRestriction'] );
|
||||
$this->assertArrayHasKey( 'PageRestrictions', $fields );
|
||||
$this->assertArrayHasKey( 'NamespaceRestrictions', $fields );
|
||||
$this->assertArrayHasKey( 'ActionRestrictions', $fields );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getFormFields
|
||||
*/
|
||||
public function testGetFormFieldsCodex(): void {
|
||||
$this->overrideConfigValues( [
|
||||
MainConfigNames::BlockAllowsUTEdit => true,
|
||||
MainConfigNames::EnablePartialActionBlocks => true,
|
||||
MainConfigNames::UseCodexSpecialBlock => true,
|
||||
] );
|
||||
$page = $this->newSpecialPage();
|
||||
$wrappedPage = TestingAccessWrapper::newFromObject( $page );
|
||||
$fields = $wrappedPage->getFormFields();
|
||||
$this->assertIsArray( $fields );
|
||||
$this->assertArrayHasKey( 'options-messages', $fields[ 'EditingRestriction' ] );
|
||||
$this->assertArrayHasKey( 'option-descriptions-messages', $fields[ 'EditingRestriction' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getFormFields
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue