Skip to content

Commit

Permalink
Create attribute within the new product MVP (woocommerce#35100)
Browse files Browse the repository at this point in the history
* Add initial add new option

* Hook in create attribute modal to add attribute field

* Add unit tests for the create attribute modal

* Add extra test to attribute input field

* Add changelog

* Add custom attribute term input field and support for custom attributes

* Fix tets

* Add track for custom attribute creation

* Fix changes after merge conflict

* Revert one change

* Fix lint error

* Seperate out some logic and make use of null as empty object versus id: undefined

* Add isNewAttributeListItem helper function

* Make use of helper function for create new markup
  • Loading branch information
louwie17 authored Nov 21, 2022
1 parent 67ce248 commit 7ec3210
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ import {
*/
import './add-attribute-modal.scss';
import { AttributeInputField } from '../attribute-input-field';
import { AttributeTermInputField } from '../attribute-term-input-field';
import {
AttributeTermInputField,
CustomAttributeTermInputField,
} from '../attribute-term-input-field';
import { HydratedAttributeType } from '../attribute-field';
import { getProductAttributeObject } from './utils';

type AddAttributeModalProps = {
onCancel: () => void;
Expand All @@ -32,7 +36,7 @@ type AddAttributeModalProps = {
};

type AttributeForm = {
attributes: Array< HydratedAttributeType | { id: undefined; terms: [] } >;
attributes: Array< HydratedAttributeType | null >;
};

export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
Expand All @@ -48,21 +52,25 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
value: AttributeForm[ keyof AttributeForm ]
) => void
) => {
setValue( 'attributes', [
...values.attributes,
{
id: undefined,
terms: [],
},
] );
setValue( 'attributes', [ ...values.attributes, null ] );
};

const onAddingAttributes = ( values: AttributeForm ) => {
const newAttributesToAdd: HydratedAttributeType[] = [];
values.attributes.forEach( ( attr ) => {
if ( attr.id && attr.name && ( attr.terms || [] ).length > 0 ) {
if (
attr !== null &&
attr.name &&
( ( attr.terms || [] ).length > 0 ||
( attr.options || [] ).length > 0 )
) {
const options =
attr.id !== 0
? ( attr.terms || [] ).map( ( term ) => term.name )
: attr.options;
newAttributesToAdd.push( {
...( attr as HydratedAttributeType ),
options,
} );
}
} );
Expand All @@ -83,28 +91,28 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
values.attributes.filter( ( val, i ) => i !== index )
);
} else {
setValue( `attributes[${ index }]`, [
{ id: undefined, terms: [] },
] );
setValue( `attributes[${ index }]`, [ null ] );
}
};

const focusValueField = ( index: number ) => {
const valueInputField: HTMLInputElement | null = document.querySelector(
'.woocommerce-add-attribute-modal__table-row-' +
index +
' .woocommerce-add-attribute-modal__table-attribute-value-column .woocommerce-experimental-select-control__input'
);
if ( valueInputField ) {
setTimeout( () => {
setTimeout( () => {
const valueInputField: HTMLInputElement | null =
document.querySelector(
'.woocommerce-add-attribute-modal__table-row-' +
index +
' .woocommerce-add-attribute-modal__table-attribute-value-column .woocommerce-experimental-select-control__input'
);
if ( valueInputField ) {
valueInputField.focus();
}, 0 );
}
}
}, 0 );
};

const onClose = ( values: AttributeForm ) => {
const hasValuesSet = values.attributes.some(
( value ) => value?.id && value?.terms && value?.terms.length > 0
( value ) =>
value !== null && value?.terms && value?.terms.length > 0
);
if ( hasValuesSet ) {
setShowConfirmClose( true );
Expand All @@ -117,7 +125,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
<>
<Form< AttributeForm >
initialValues={ {
attributes: [ { id: undefined, terms: [] } ],
attributes: [ null ],
} }
>
{ ( {
Expand Down Expand Up @@ -162,7 +170,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
</thead>
<tbody>
{ values.attributes.map(
( formAttr, index ) => (
( attribute, index ) => (
<tr
key={ index }
className={ `woocommerce-add-attribute-modal__table-row woocommerce-add-attribute-modal__table-row-${ index }` }
Expand All @@ -173,25 +181,18 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
'Search or create attribute',
'woocommerce'
) }
value={
formAttr.id &&
formAttr.name
? formAttr
: null
}
value={ attribute }
onChange={ (
val
) => {
setValue(
'attributes[' +
index +
']',
{
...val,
terms: [],
options:
undefined,
}
val &&
getProductAttributeObject(
val
)
);
if ( val ) {
focusValueField(
Expand Down Expand Up @@ -219,31 +220,64 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
/>
</td>
<td className="woocommerce-add-attribute-modal__table-attribute-value-column">
<AttributeTermInputField
placeholder={ __(
'Search or create value',
'woocommerce'
) }
disabled={
! formAttr.id
}
attributeId={
formAttr.id
}
value={
formAttr.terms
}
onChange={ (
val
) =>
setValue(
'attributes[' +
index +
'].terms',
{ attribute === null ||
attribute.id !== 0 ? (
<AttributeTermInputField
placeholder={ __(
'Search or create value',
'woocommerce'
) }
disabled={
attribute
? ! attribute.id
: true
}
attributeId={
attribute
? attribute.id
: undefined
}
value={
attribute ===
null
? []
: attribute.terms
}
onChange={ (
val
)
}
/>
) =>
setValue(
'attributes[' +
index +
'].terms',
val
)
}
/>
) : (
<CustomAttributeTermInputField
placeholder={ __(
'Search or create value',
'woocommerce'
) }
disabled={
! attribute.name
}
value={
attribute.options
}
onChange={ (
val
) =>
setValue(
'attributes[' +
index +
'].options',
val
)
}
/>
) }
</td>
<td className="woocommerce-add-attribute-modal__table-attribute-trash-column">
<Button
Expand All @@ -253,9 +287,9 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
.attributes
.length ===
1 &&
! values
.attributes[ 0 ]
?.id
values
.attributes[ 0 ] ===
null
}
label={ __(
'Remove attribute',
Expand Down Expand Up @@ -308,9 +342,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
) }
disabled={
values.attributes.length === 1 &&
! values.attributes[ 0 ]?.id &&
values.attributes[ 0 ]?.terms
?.length === 0
values.attributes[ 0 ] === null
}
onClick={ () =>
onAddingAttributes( values )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,10 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
...newAttributes
.filter(
( newAttr ) =>
! ( value || [] ).find(
( attr ) => attr.id === newAttr.id
! ( value || [] ).find( ( attr ) =>
newAttr.id === 0
? newAttr.name === attr.name // check name if custom attribute = id === 0.
: attr.id === newAttr.id
)
)
.map( ( newAttr, index ) => {
Expand Down Expand Up @@ -287,7 +289,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
selectedAttributeIds={ value.map( ( attr ) => attr.id ) }
/>
) }

<SelectControlMenuSlot />
{ editingAttributeId && (
<EditAttributeModal
onCancel={ () => setEditingAttributeId( null ) }
Expand Down Expand Up @@ -315,7 +317,6 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
}
/>
) }
<SelectControlMenuSlot />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,10 @@ describe( 'AddAttributeModal', () => {
/>
);

attributeOnChange( attributeList[ 0 ] );
attributeOnChange( {
...attributeList[ 0 ],
options: [],
} );
queryByRole( 'button', { name: 'Add attributes' } )?.click();
expect( onAddMock ).toHaveBeenCalledWith( [] );
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,22 @@ export function reorderSortableProductAttributePositions(
} )
.filter( ( attr ): attr is ProductAttribute => attr !== undefined );
}

/**
* Helper function to return the product attribute object. If attribute is a string it will create an object.
*
* @param { Object | string } attribute product attribute as string or object.
*/
export function getProductAttributeObject(
attribute:
| string
| Omit< ProductAttribute, 'position' | 'visible' | 'variation' >
): Omit< ProductAttribute, 'position' | 'visible' | 'variation' > {
return typeof attribute === 'string'
? {
id: 0,
name: attribute,
options: [],
}
: attribute;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.woocommerce-attribute-input-field {
&__add-new {
display: flex;
align-items: center;
font-weight: 600;
}
&__add-new-icon {
margin-right: $gap-small;
}
}
Loading

0 comments on commit 7ec3210

Please sign in to comment.