-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add: Media field changing ui to Dataviews and content preview field to posts and pages #67278
Changes from 8 commits
cfe2cc0
e5f07a3
5de23a1
a604caf
fd31448
38e8ece
757d1fc
24bbb1d
722ac27
f713ffe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import type { ChangeEvent } from 'react'; | ||
import type { ChangeEvent, ReactNode } from 'react'; | ||
import clsx from 'clsx'; | ||
|
||
/** | ||
* WordPress dependencies | ||
|
@@ -26,14 +27,15 @@ import { | |
Icon, | ||
} from '@wordpress/components'; | ||
import { __, _x, sprintf } from '@wordpress/i18n'; | ||
import { memo, useContext, useMemo } from '@wordpress/element'; | ||
import { memo, useContext, useMemo, useState } from '@wordpress/element'; | ||
import { | ||
chevronDown, | ||
chevronUp, | ||
cog, | ||
seen, | ||
unseen, | ||
lock, | ||
moreVertical, | ||
} from '@wordpress/icons'; | ||
import warning from '@wordpress/warning'; | ||
import { useInstanceId } from '@wordpress/compose'; | ||
|
@@ -253,25 +255,77 @@ function ItemsPerPageControl() { | |
); | ||
} | ||
|
||
function PreviewOptions( { | ||
previewOptions, | ||
onChangePreviewOption, | ||
onMenuOpenChange, | ||
activeOption, | ||
}: { | ||
previewOptions?: Array< { label: string; id: string } >; | ||
onChangePreviewOption?: ( newPreviewOption: string ) => void; | ||
onMenuOpenChange: ( isOpen: boolean ) => void; | ||
activeOption?: string; | ||
} ) { | ||
return ( | ||
<Menu onOpenChange={ onMenuOpenChange }> | ||
<Menu.TriggerButton | ||
render={ | ||
<Button | ||
size="compact" | ||
icon={ moreVertical } | ||
label={ __( 'Preview' ) } | ||
/> | ||
} | ||
/> | ||
<Menu.Popover> | ||
{ previewOptions?.map( ( { id, label } ) => { | ||
return ( | ||
<Menu.RadioItem | ||
key={ id } | ||
value={ id } | ||
checked={ id === activeOption } | ||
onChange={ () => { | ||
onChangePreviewOption?.( id ); | ||
} } | ||
> | ||
<Menu.ItemLabel>{ label }</Menu.ItemLabel> | ||
</Menu.RadioItem> | ||
); | ||
} ) } | ||
</Menu.Popover> | ||
</Menu> | ||
); | ||
} | ||
function FieldItem( { | ||
field, | ||
label, | ||
description, | ||
isVisible, | ||
isFirst, | ||
isLast, | ||
canMove = true, | ||
onToggleVisibility, | ||
onMoveUp, | ||
onMoveDown, | ||
previewOptions, | ||
onChangePreviewOption, | ||
}: { | ||
field: NormalizedField< any >; | ||
label?: string; | ||
description?: string; | ||
isVisible: boolean; | ||
isFirst?: boolean; | ||
isLast?: boolean; | ||
canMove?: boolean; | ||
onToggleVisibility?: () => void; | ||
onMoveUp?: () => void; | ||
onMoveDown?: () => void; | ||
previewOptions?: Array< { label: string; id: string } >; | ||
onChangePreviewOption?: ( newPreviewOption: string ) => void; | ||
} ) { | ||
const [ isChangingPreviewOption, setIsChangingPreviewOption ] = | ||
useState< boolean >( false ); | ||
|
||
const focusVisibilityField = () => { | ||
// Focus the visibility button to avoid focus loss. | ||
// Our code is safe against the component being unmounted, so we don't need to worry about cleaning the timeout. | ||
|
@@ -290,16 +344,33 @@ function FieldItem( { | |
<Item> | ||
<HStack | ||
expanded | ||
className={ `dataviews-field-control__field dataviews-field-control__field-${ field.id }` } | ||
className={ clsx( | ||
'dataviews-field-control__field', | ||
`dataviews-field-control__field-${ field.id }`, | ||
// The actions are hidden when the mouse is not hovering the item, or focus | ||
// is outside the item. | ||
// For actions that require a popover, a menu etc, that would mean that when the interactive element | ||
// opens and the focus goes there the actions would be hidden. | ||
// To avoid that we add a class to the item, that makes sure actions are visible while there is some | ||
// interaction with the item. | ||
{ 'is-interacting': isChangingPreviewOption } | ||
) } | ||
justify="flex-start" | ||
> | ||
<span className="dataviews-field-control__icon"> | ||
{ ! canMove && ! field.enableHiding && ( | ||
<Icon icon={ lock } /> | ||
) } | ||
</span> | ||
<span className="dataviews-field-control__label"> | ||
{ field.label } | ||
<span className="dataviews-field-control__label-sub-label-container"> | ||
<span className="dataviews-field-control__label"> | ||
{ label || field.label } | ||
</span> | ||
{ description && ( | ||
<span className="dataviews-field-control__sub-label"> | ||
{ description } | ||
</span> | ||
) } | ||
</span> | ||
<HStack | ||
justify="flex-end" | ||
|
@@ -368,6 +439,14 @@ function FieldItem( { | |
} | ||
/> | ||
) } | ||
{ previewOptions && ( | ||
<PreviewOptions | ||
previewOptions={ previewOptions } | ||
onChangePreviewOption={ onChangePreviewOption } | ||
onMenuOpenChange={ setIsChangingPreviewOption } | ||
activeOption={ field.id } | ||
/> | ||
) } | ||
</HStack> | ||
</HStack> | ||
</Item> | ||
|
@@ -461,7 +540,8 @@ function FieldControl() { | |
const hiddenFields = fields.filter( | ||
( f ) => | ||
! visibleFieldIds.includes( f.id ) && | ||
! togglableFields.includes( f.id ) | ||
! togglableFields.includes( f.id ) && | ||
f.type !== 'media' | ||
); | ||
const visibleFields = visibleFieldIds | ||
.map( ( fieldId ) => fields.find( ( f ) => f.id === fieldId ) ) | ||
|
@@ -471,18 +551,50 @@ function FieldControl() { | |
return null; | ||
} | ||
const titleField = fields.find( ( f ) => f.id === view.titleField ); | ||
const mediaField = fields.find( ( f ) => f.id === view.mediaField ); | ||
const previewField = fields.find( ( f ) => f.id === view.mediaField ); | ||
const descriptionField = fields.find( | ||
( f ) => f.id === view.descriptionField | ||
); | ||
|
||
const previewFields = fields.filter( ( f ) => f.type === 'media' ); | ||
|
||
let previewFieldUI; | ||
if ( previewFields.length > 1 ) { | ||
const isPreviewFieldVisible = | ||
isDefined( previewField ) && ( view.showMedia ?? true ); | ||
previewFieldUI = isDefined( previewField ) && ( | ||
<FieldItem | ||
key={ previewField.id } | ||
field={ previewField } | ||
label={ __( 'Preview' ) } | ||
description={ previewField.label } | ||
isVisible={ isPreviewFieldVisible } | ||
onToggleVisibility={ () => { | ||
onChangeView( { | ||
...view, | ||
showMedia: ! isPreviewFieldVisible, | ||
} ); | ||
} } | ||
canMove={ false } | ||
previewOptions={ previewFields.map( ( field ) => ( { | ||
label: field.label, | ||
id: field.id, | ||
} ) ) } | ||
onChangePreviewOption={ ( newPreviewId ) => | ||
onChangeView( { ...view, mediaField: newPreviewId } ) | ||
} | ||
/> | ||
); | ||
} | ||
const lockedFields = [ | ||
{ | ||
field: titleField, | ||
isVisibleFlag: 'showTitle', | ||
}, | ||
{ | ||
field: mediaField, | ||
field: previewField, | ||
isVisibleFlag: 'showMedia', | ||
ui: previewFieldUI, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to simplify this implementation? For example, instead of adding a specific UI for a field, what if we made the following changes: FieldItem:
In the <FieldItem
field={ field }
description={ view.mediaField.label }
options=[
{ label:"Content preview", id: content_preview },
{ label: "Featured image", id: feature_image }
],
onChangeOption={ newOption => onChangeView({ mediaField: newOption })
/> I feel the implementation would be simpler if we make the FieldItem (leaf node) a general component that the FieldControl configures depending on the specifics of the field (visible, not visible, can have multiple options, etc.). This way, it's easy to add the same behaviour to other fields (title). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I applied this suggestion, It had some complexities but the base idea is there. |
||
}, | ||
{ | ||
field: descriptionField, | ||
|
@@ -493,12 +605,20 @@ function FieldControl() { | |
( { field, isVisibleFlag } ) => | ||
// @ts-expect-error | ||
isDefined( field ) && ( view[ isVisibleFlag ] ?? true ) | ||
) as Array< { field: NormalizedField< any >; isVisibleFlag: string } >; | ||
) as Array< { | ||
field: NormalizedField< any >; | ||
isVisibleFlag: string; | ||
ui?: ReactNode; | ||
} >; | ||
const hiddenLockedFields = lockedFields.filter( | ||
( { field, isVisibleFlag } ) => | ||
// @ts-expect-error | ||
isDefined( field ) && ! ( view[ isVisibleFlag ] ?? true ) | ||
) as Array< { field: NormalizedField< any >; isVisibleFlag: string } >; | ||
) as Array< { | ||
field: NormalizedField< any >; | ||
isVisibleFlag: string; | ||
ui?: ReactNode; | ||
} >; | ||
|
||
return ( | ||
<VStack className="dataviews-field-control" spacing={ 6 }> | ||
|
@@ -507,20 +627,22 @@ function FieldControl() { | |
!! visibleFields?.length ) && ( | ||
<ItemGroup isBordered isSeparated> | ||
{ visibleLockedFields.map( | ||
( { field, isVisibleFlag } ) => { | ||
( { field, isVisibleFlag, ui } ) => { | ||
return ( | ||
<FieldItem | ||
key={ field.id } | ||
field={ field } | ||
isVisible | ||
onToggleVisibility={ () => { | ||
onChangeView( { | ||
...view, | ||
[ isVisibleFlag ]: false, | ||
} ); | ||
} } | ||
canMove={ false } | ||
/> | ||
ui ?? ( | ||
<FieldItem | ||
key={ field.id } | ||
field={ field } | ||
isVisible | ||
onToggleVisibility={ () => { | ||
onChangeView( { | ||
...view, | ||
[ isVisibleFlag ]: false, | ||
} ); | ||
} } | ||
canMove={ false } | ||
/> | ||
) | ||
); | ||
} | ||
) } | ||
|
@@ -550,20 +672,23 @@ function FieldControl() { | |
<ItemGroup isBordered isSeparated> | ||
{ hiddenLockedFields.length > 0 && | ||
hiddenLockedFields.map( | ||
( { field, isVisibleFlag } ) => { | ||
( { field, isVisibleFlag, ui } ) => { | ||
return ( | ||
<FieldItem | ||
key={ field.id } | ||
field={ field } | ||
isVisible={ false } | ||
onToggleVisibility={ () => { | ||
onChangeView( { | ||
...view, | ||
[ isVisibleFlag ]: true, | ||
} ); | ||
} } | ||
canMove={ false } | ||
/> | ||
ui ?? ( | ||
<FieldItem | ||
key={ field.id } | ||
field={ field } | ||
isVisible={ false } | ||
onToggleVisibility={ () => { | ||
onChangeView( { | ||
...view, | ||
[ isVisibleFlag ]: | ||
true, | ||
} ); | ||
} } | ||
canMove={ false } | ||
/> | ||
) | ||
); | ||
} | ||
) } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,7 +42,7 @@ export type Operator = | |
| 'isAll' | ||
| 'isNotAll'; | ||
|
||
export type FieldType = 'text' | 'integer' | 'datetime'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, this comes with its own field-type definition. However, we don't need that for this PR. To control scope, we can do it in a follow-up. There're other changes we may want to do for a |
||
export type FieldType = 'text' | 'integer' | 'datetime' | 'media'; | ||
|
||
export type ValidationContext = { | ||
elements?: Option[]; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove this and add more individual props? If anything we should aim for the opposite if possible IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @ntsekouras, because the "Preview" item we create with a label sublabel/description etc is not a field, this component receives strings that can come from real field properties.