Skip to content

Commit 6809e9f

Browse files
jenkins-botGerrit Code Review
authored andcommitted
Merge "Edit and delete snaks on references"
2 parents 136639c + 74babb6 commit 6809e9f

16 files changed

+862
-181
lines changed

cypress/e2e/wbui2025/addReference.cy.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Util } from 'cypress-wikibase-api';
33
import { ItemViewPage } from '../../support/pageObjects/ItemViewPage';
44
import { EditStatementFormPage } from '../../support/pageObjects/EditStatementFormPage';
55
import { AddReferenceFormPage } from '../../support/pageObjects/AddReferenceFormPage';
6+
import { ValueForm } from '../../support/pageObjects/ValueForm';
67

78
describe( 'wbui2025 add reference', () => {
89
context( 'mobile view', () => {
@@ -52,7 +53,11 @@ describe( 'wbui2025 add reference', () => {
5253
addReferenceFormPage.setSnakValue( referenceSnakValue );
5354
addReferenceFormPage.addButton().click();
5455

55-
editStatementFormPage.valueForms().should( 'contain.text', referenceSnakValue );
56+
editStatementFormPage.references().first().then( ( element ) => {
57+
const valueForm = new ValueForm( element );
58+
valueForm.textInput().should( 'have.value', referenceSnakValue );
59+
} );
60+
5661
editStatementFormPage.publishButton().click();
5762
editStatementFormPage.form().should( 'not.exist' );
5863

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { Util } from 'cypress-wikibase-api';
2+
3+
import { EditStatementFormPage } from '../../support/pageObjects/EditStatementFormPage';
4+
import { ItemViewPage } from '../../support/pageObjects/ItemViewPage';
5+
import { LoginPage } from '../../support/pageObjects/LoginPage';
6+
import { ValueForm } from '../../support/pageObjects/ValueForm';
7+
8+
describe( 'wbui2025 edit references', () => {
9+
context( 'mobile view', () => {
10+
const snakToDelete = 'single snak to delete';
11+
12+
beforeEach( () => {
13+
const loginPage = new LoginPage();
14+
cy.task(
15+
'MwApi:CreateUser',
16+
{ usernamePrefix: 'mextest' },
17+
).then( ( { username, password } ) => {
18+
loginPage.login( username, password );
19+
} );
20+
21+
cy.task( 'MwApi:GetOrCreatePropertyIdByDataType', { datatype: 'string' } )
22+
.then( ( propertyId: string ) => {
23+
cy.wrap( propertyId ).as( 'propertyId' );
24+
const statementData = {
25+
claims: [ {
26+
mainsnak: {
27+
snaktype: 'value',
28+
property: propertyId,
29+
datavalue: {
30+
value: 'example string value',
31+
type: 'string',
32+
},
33+
},
34+
type: 'statement',
35+
rank: 'normal',
36+
} ],
37+
};
38+
cy.task( 'MwApi:CreateItem', { label: Util.getTestString( 'item' ), data: statementData } )
39+
.then( ( itemId: string ) => {
40+
cy.wrap( itemId ).as( 'itemId' );
41+
} );
42+
cy.get( '@itemId' ).then( ( itemId ) => {
43+
cy.task( 'MwApi:GetEntityData', { entityId: itemId } ).then( ( data ) => {
44+
const snaks1 = {
45+
[ propertyId ]: [ {
46+
snaktype: 'value',
47+
property: propertyId,
48+
datavalue: {
49+
value: snakToDelete,
50+
type: 'string',
51+
},
52+
datatype: 'string',
53+
} ],
54+
};
55+
56+
const snaks2 = {
57+
[ propertyId ]: [
58+
{
59+
snaktype: 'value',
60+
property: propertyId,
61+
datavalue: {
62+
value: 'first snak',
63+
type: 'string',
64+
},
65+
datatype: 'string',
66+
}, {
67+
snaktype: 'value',
68+
property: propertyId,
69+
datavalue: {
70+
value: 'second snak',
71+
type: 'string',
72+
},
73+
datatype: 'string',
74+
},
75+
],
76+
};
77+
78+
cy.task( 'MwApi:BotRequest', {
79+
isEdit: true,
80+
isPost: true,
81+
parameters: {
82+
action: 'wbsetreference',
83+
statement: data.claims[ propertyId ][ 0 ].id,
84+
snaks: JSON.stringify( snaks1 ),
85+
},
86+
} );
87+
cy.task( 'MwApi:BotRequest', {
88+
isEdit: true,
89+
isPost: true,
90+
parameters: {
91+
action: 'wbsetreference',
92+
statement: data.claims[ propertyId ][ 0 ].id,
93+
snaks: JSON.stringify( snaks2 ),
94+
},
95+
} );
96+
} );
97+
} );
98+
} );
99+
cy.viewport( 375, 1280 );
100+
} );
101+
it( 'references are editable and deletable', () => {
102+
cy.get( '@itemId' ).then( ( itemId ) => {
103+
const itemViewPage = new ItemViewPage( itemId );
104+
itemViewPage.open().statementsSection();
105+
itemViewPage.editLinks().first().click();
106+
const editFormPage = new EditStatementFormPage();
107+
editFormPage.referencesAccordion().click();
108+
109+
editFormPage.references().should( 'have.length', 2 );
110+
111+
const editedSnakValue = Util.getTestString( 'edited-snak' );
112+
editFormPage.valueForms().first().within( () => {
113+
editFormPage.references().eq( 1 ).within( ( element ) => {
114+
const valueForm = new ValueForm( element );
115+
valueForm.textInput().first().type( '{selectAll}{backspace}' + editedSnakValue );
116+
} );
117+
118+
editFormPage.references().first().within( ( element ) => {
119+
( new ValueForm( element ) ).removeSnakButton().first().click();
120+
} );
121+
} );
122+
123+
editFormPage.publishButton().click();
124+
125+
// Verify the edits
126+
editFormPage.valueForms().should( 'not.exist' );
127+
itemViewPage.referencesSections().first().then( ( element ) => {
128+
itemViewPage.referencesAccordion( element ).click();
129+
itemViewPage.references( element ).should( 'have.length', 1 );
130+
itemViewPage.references( element ).should( 'not.contain.text', snakToDelete );
131+
itemViewPage.references( element ).should( 'contain.text', editedSnakValue );
132+
} );
133+
134+
cy.task( 'MwApi:GetEntityData', { entityId: itemId } ).then( ( data ) => {
135+
cy.get( '@propertyId' ).then( ( propertyId ) => {
136+
cy.wrap( data.claims[ propertyId ][ 0 ].references )
137+
.should( 'have.length', 1 );
138+
cy.wrap( data.claims[ propertyId ][ 0 ].references[ 0 ].snaks[ propertyId ] )
139+
.should( 'have.length', 2 );
140+
} );
141+
} );
142+
143+
// Edit again, delete the reference with 2 snaks
144+
itemViewPage.editLinks().first().click();
145+
editFormPage.referencesAccordion().click();
146+
editFormPage.references().should( 'have.length', 1 );
147+
editFormPage.valueForms().first().then( ( element: HTMLElement ) => {
148+
const valueForm = new ValueForm( element );
149+
editFormPage.references().first().within( () => {
150+
valueForm.removeReferenceButton().click();
151+
} );
152+
} );
153+
154+
editFormPage.publishButton().click();
155+
156+
// Verify the edits
157+
editFormPage.valueForms().should( 'not.exist' );
158+
itemViewPage.statementsSection().should( 'contain.text', '0 references' );
159+
160+
cy.task( 'MwApi:GetEntityData', { entityId: itemId } ).then( ( data ) => {
161+
cy.get( '@propertyId' ).then( ( propertyId ) => {
162+
cy.wrap( data.claims[ propertyId ][ 0 ].references ).should( 'be.undefined' );
163+
} );
164+
} );
165+
166+
} );
167+
} );
168+
} );
169+
} );

cypress/support/pageObjects/EditStatementFormPage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export class EditStatementFormPage {
1616
MENU_ITEM: '.wikibase-wbui2025-edit-statement-value-input .cdx-menu-item',
1717
RANK_SELECT: '.wikibase-wbui2025-rank-input .cdx-select-vue',
1818
ADD_REFERENCE_BUTTON: '.wikibase-wbui2025-add-reference-button',
19+
REFERENCES: '.wikibase-wbui2025-editable-reference',
20+
REFERENCES_ACCORDION: '.wikibase-wbui2025-editable-references-section .cdx-accordion summary',
1921
};
2022

2123
public static FORM_HEADING = '.wikibase-wbui2025-edit-statement-heading';
@@ -80,6 +82,14 @@ export class EditStatementFormPage {
8082
return cy.get( EditStatementFormPage.SELECTORS.MENU_ITEM );
8183
}
8284

85+
public references(): Chainable {
86+
return cy.get( EditStatementFormPage.SELECTORS.REFERENCES );
87+
}
88+
89+
public referencesAccordion(): Chainable {
90+
return cy.get( EditStatementFormPage.SELECTORS.REFERENCES_ACCORDION );
91+
}
92+
8393
public getLookupComponentSelector(): string {
8494
return EditStatementFormPage.SELECTORS.LOOKUP_COMPONENT;
8595
}

cypress/support/pageObjects/ItemViewPage.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ export class ItemViewPage {
3939
return this;
4040
}
4141

42-
public statementsSection(): this {
43-
cy.get( ItemViewPage.STATEMENTS );
44-
return this;
42+
public statementsSection(): Chainable {
43+
return cy.get( ItemViewPage.STATEMENTS );
4544
}
4645

4746
public editLinks(): Chainable {

cypress/support/pageObjects/ValueForm.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export class ValueForm {
1818

1919
public static NO_VALUE_SOME_VALUE_PLACEHOLDER = '.wikibase-wbui2025-novalue-somevalue-holder';
2020

21+
public static TEXT_INPUT = '.cdx-text-input input';
22+
23+
public static REMOVE_SNAK_BUTTON = '.wikibase-wbui2025-remove-snak';
24+
25+
public static REMOVE_REFERENCE_BUTTON = '.wikibase-wbui2025-editable-reference-remove-button-holder button';
26+
2127
private rootElement: HTMLElement;
2228

2329
public constructor( rootElement: HTMLElement ) {
@@ -57,4 +63,16 @@ export class ValueForm {
5763
public noValueSomeValuePlaceholder(): Chainable {
5864
return cy.get( ValueForm.NO_VALUE_SOME_VALUE_PLACEHOLDER, { withinSubject: this.rootElement } );
5965
}
66+
67+
public textInput(): Chainable {
68+
return cy.get( ValueForm.TEXT_INPUT, { withinSubject: this.rootElement } );
69+
}
70+
71+
public removeSnakButton(): Chainable {
72+
return cy.get( ValueForm.REMOVE_SNAK_BUTTON, { withinSubject: this.rootElement } );
73+
}
74+
75+
public removeReferenceButton(): Chainable {
76+
return cy.get( ValueForm.REMOVE_REFERENCE_BUTTON, { withinSubject: this.rootElement } );
77+
}
6078
}

repo/includes/RepoHooks.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,8 @@ public function onResourceLoaderRegisterModules( $rl ): void {
10021002
'resources/wikibase.wbui2025/wikibase.wbui2025.propertySelector.vue',
10031003
'resources/wikibase.wbui2025/wikibase.wbui2025.modalOverlay.vue',
10041004
'resources/wikibase.wbui2025/wikibase.wbui2025.editableQualifiers.vue',
1005+
'resources/wikibase.wbui2025/wikibase.wbui2025.editableReference.vue',
1006+
'resources/wikibase.wbui2025/wikibase.wbui2025.editableReferencesSection.vue',
10051007
'resources/wikibase.wbui2025/wikibase.wbui2025.editableSnak.vue',
10061008
'resources/wikibase.wbui2025/wikibase.wbui2025.editableSnakValue.vue',
10071009
'resources/wikibase.wbui2025/wikibase.wbui2025.editStatementGroup.vue',
@@ -1074,6 +1076,7 @@ public function onResourceLoaderRegisterModules( $rl ): void {
10741076
'wikibase-statementview-references-counter',
10751077
],
10761078
'codexComponents' => [
1079+
'CdxAccordion',
10771080
'CdxButton',
10781081
'CdxIcon',
10791082
'CdxLookup',

repo/resources/wikibase.wbui2025/store/editStatementsStore.js

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,33 @@ const useEditStatementStore = ( statementId ) => defineStore( 'editStatement-' +
128128
} );
129129
}
130130
this.qualifiersOrder = ( statementData[ 'qualifiers-order' ] || [] ).slice( 0 );
131-
this.references = ( statementData.references || [] ).slice( 0 );
131+
this.references = ( statementData.references || [] ).map( ( reference ) => {
132+
const tempReference = {
133+
hash: reference.hash,
134+
snaks: {},
135+
'snaks-order': ( reference[ 'snaks-order' ] || [] ).slice( 0 )
136+
};
137+
for ( const [ snakPropertyId, snakList ] of Object.entries( reference.snaks || {} ) ) {
138+
tempReference.snaks[ snakPropertyId ] = snakList.map( ( snak ) => {
139+
const snakKey = generateNextSnakKey();
140+
useEditSnakStore( snakKey )().initializeWithSnak( snak );
141+
return snakKey;
142+
} );
143+
}
144+
return tempReference;
145+
} );
132146
},
133147

134148
disposeOfStatementStoreAndSnaks() {
135149
deleteStore( useEditSnakStore( this.mainSnakKey )() );
136150
for ( const [ , statementList ] of Object.entries( this.qualifiers ) ) {
137151
statementList.forEach( ( snakKey ) => deleteStore( useEditSnakStore( snakKey )() ) );
138152
}
139-
// TODO: T405236 Also dispose of reference snak data here
153+
for ( const reference of this.references ) {
154+
for ( const [ , statementList ] of Object.entries( reference.snaks || {} ) ) {
155+
statementList.forEach( ( snakKey ) => deleteStore( useEditSnakStore( snakKey )() ) );
156+
}
157+
}
140158
deleteStore( this );
141159
}
142160
},
@@ -160,7 +178,19 @@ const useEditStatementStore = ( statementId ) => defineStore( 'editStatement-' +
160178
}
161179
}
162180
}
163-
// TODO check references once they use wbparsevalue (T406887)
181+
for ( const reference of state.references ) {
182+
for ( const propertyId of reference[ 'snaks-order' ] ) {
183+
if ( !Array.isArray( ( reference.snaks || {} )[ propertyId ] ) ) {
184+
continue;
185+
}
186+
for ( const snakKey of reference.snaks[ propertyId ] ) {
187+
const referenceSnakState = useEditSnakStore( snakKey )();
188+
if ( !snakFullyParsed( referenceSnakState ) ) {
189+
return false;
190+
}
191+
}
192+
}
193+
}
164194
return true;
165195
},
166196
/**
@@ -234,12 +264,12 @@ const useEditStatementStore = ( statementId ) => defineStore( 'editStatement-' +
234264
return true;
235265
}
236266
for ( let j = 0; j < referenceSnaks.length; j++ ) {
237-
const referenceSnak = referenceSnaks[ j ];
267+
const referenceSnak = useEditSnakStore( referenceSnaks[ j ] )();
238268
const savedReferenceSnak = savedReferenceSnaks[ j ];
239269
if ( referenceSnak.snaktype !== savedReferenceSnak.snaktype ) {
240270
return true;
241271
}
242-
if ( referenceSnak.snaktype === 'value' && !sameDataValue( referenceSnak.datavalue, savedReferenceSnak.datavalue ) ) {
272+
if ( referenceSnak.snaktype === 'value' && !sameDataValue( referenceSnak.currentDataValue(), savedReferenceSnak.datavalue ) ) {
243273
return true;
244274
}
245275
}
@@ -309,10 +339,23 @@ const useEditStatementsStore = defineStore( 'editStatements', {
309339
async ( snakHash ) => await useEditSnakStore( snakHash )().buildSnakJson()
310340
) );
311341
}
342+
343+
const builtReferencesData = await Promise.all( editStatementStore.references.map( async ( reference ) => {
344+
const builtReference = {
345+
'snaks-order': reference[ 'snaks-order' ],
346+
snaks: {}
347+
};
348+
for ( const [ propertyId, statementList ] of Object.entries( reference.snaks ) ) {
349+
builtReference.snaks[ propertyId ] = await Promise.all( statementList.map(
350+
async ( snakHash ) => await useEditSnakStore( snakHash )().buildSnakJson()
351+
) );
352+
}
353+
return builtReference;
354+
} ) );
312355
return {
313356
id: statementId,
314357
mainsnak: await mainSnakStore.buildSnakJson(),
315-
references: editStatementStore.references,
358+
references: builtReferencesData,
316359
'qualifiers-order': editStatementStore.qualifiersOrder,
317360
qualifiers: builtQualifierData,
318361
type: 'statement',

0 commit comments

Comments
 (0)