Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2e9e361
Add EntityStatus and EntityVisibility enums
JoryHogeveen Aug 16, 2026
d2575b8
Move dependents delete validation to EngineModel where it belongs
JoryHogeveen Aug 16, 2026
e529221
Add EngineEntity magic methods
JoryHogeveen Aug 16, 2026
089e7bd
Add trash and restore methods, set trashed timestamp as private key
JoryHogeveen Aug 16, 2026
1803cdc
Parse query with status and visibility defaults
JoryHogeveen Aug 16, 2026
4de1623
Add state method helpers and get trashed date method
JoryHogeveen Aug 16, 2026
51415f5
Create new entity columns for status and visibility
JoryHogeveen Aug 16, 2026
435da30
Fix missing support for enums
JoryHogeveen Aug 16, 2026
13ccbeb
Add JSON handlers for trash and restore plus add extra permanent chec…
JoryHogeveen Aug 16, 2026
6b71517
Add entitymanager params
JoryHogeveen Aug 16, 2026
a9917ec
Restore prev state when restoring
JoryHogeveen Aug 16, 2026
436eefa
Return updated entity and combine Trash and Restore actions
JoryHogeveen Aug 16, 2026
42959f3
Add disable,enable,show,hide actions
JoryHogeveen Aug 16, 2026
2fcab4a
Remove old code
JoryHogeveen Aug 16, 2026
27b18da
Add status and visibility actions in useEntities hook
JoryHogeveen Aug 17, 2026
27a498a
Add missing entity ID
JoryHogeveen Aug 17, 2026
a673e24
Add refresh for delete, trash and restore
JoryHogeveen Aug 17, 2026
d477017
Add actions for trash and restore
JoryHogeveen Aug 17, 2026
d71a168
Add trash to callbacks
JoryHogeveen Aug 17, 2026
6e662f2
Fix incorrect key
JoryHogeveen Aug 17, 2026
9a7a898
Add enable action and entity support
JoryHogeveen Aug 17, 2026
81308c1
Disabled/hidden UI for views
JoryHogeveen Aug 17, 2026
c7f5a01
Fix trigger return prop handling
JoryHogeveen Aug 17, 2026
efe4e3b
Show both enabled and disabled entities by default in UI
JoryHogeveen Aug 17, 2026
83b3ac1
Only apply defaults for EngineModel
JoryHogeveen Aug 17, 2026
1f878ff
Apply defaults unless specifically set.
JoryHogeveen Aug 17, 2026
361877c
Add status checks when executing or scheduling endpoints.
JoryHogeveen Aug 17, 2026
1f5381e
Add general entity actions
JoryHogeveen Aug 17, 2026
ea74916
Add automation status test case
JoryHogeveen Aug 17, 2026
883626e
New filters UI for ListController
JoryHogeveen Aug 17, 2026
d86b506
Default Status filter (enabled, disabled and trashed)
JoryHogeveen Aug 17, 2026
6fa20a6
Add all automation actions
JoryHogeveen Aug 17, 2026
6108c2d
Add icon support in filters
JoryHogeveen Aug 17, 2026
78ae51d
Support asterisk in actual query
JoryHogeveen Aug 17, 2026
bdda5e4
Prevent schedule if not enabled
JoryHogeveen Aug 17, 2026
dfdf53b
Add show and hide to refreshable actions
JoryHogeveen Aug 17, 2026
bb6cac4
Add show and hide actions in view
JoryHogeveen Aug 17, 2026
8116bbc
Add show and hide actions in template
JoryHogeveen Aug 17, 2026
b97e7f2
Also return normalized entity to update (except for delete action)
JoryHogeveen Aug 17, 2026
a935dd8
Also update the registry with the edited entity
JoryHogeveen Aug 17, 2026
515e9cb
Add missing status icons
JoryHogeveen Aug 17, 2026
cecd79f
Add status and visibility badges in info column
JoryHogeveen Aug 17, 2026
37410f1
Add plural badges param
JoryHogeveen Aug 17, 2026
9b71150
Remove opacity as it breaks UI for dropdowns
JoryHogeveen Aug 17, 2026
33a9d93
Rename isHidden to isElementHidden
JoryHogeveen Aug 17, 2026
5bc213a
Make sure the API always shows all entities by default, regardless of…
JoryHogeveen Aug 17, 2026
b28064d
Add delete_permanent flag in the API delete params
JoryHogeveen Aug 17, 2026
799a0de
Add isItemDisabled helper util and add "text-muted" class to disabled…
JoryHogeveen Aug 17, 2026
f43f029
Add visibility filters
JoryHogeveen Aug 17, 2026
858215e
Refactor access validation to check for editor by defualt except for …
JoryHogeveen Aug 20, 2026
17e31e0
Only expose trash in the API for now, I'll decide on how to enhance A…
JoryHogeveen Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion assets/react/components/services/Repeatable/Body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Alert, Stack } from 'react-bootstrap';
import Fields from '../../form/Fields';
import { FieldsContext } from '../../../context/FieldsContext';
import { isItemDisabled } from '../../../utils/conditions';

export default function Body( props ) {
const { t } = useTranslation();
Expand Down Expand Up @@ -42,7 +43,7 @@ export default function Body( props ) {
} : {};

return (
<Stack gap={ 2 } className={ ( ( props._disabled ?? false ) ? 'opacity-50' : '' ) }>
<Stack gap={ 2 } className={ ( ( isItemDisabled( props ) ) ? 'opacity-50' : '' ) }>
{ description }
<Fields { ...props } fields={ { ...labelFields, ...props.fields } } fieldsContext={ useContext( FieldsContext ) } />
</Stack>
Expand Down
3 changes: 2 additions & 1 deletion assets/react/components/services/Repeatable/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { Stack } from 'react-bootstrap';
import Info from '../../views/Blocks/Info';
import ListRow from '../../views/List/Row';
import { mergeClassNames } from '../../../utils/props';
import { isItemDisabled } from '../../../utils/conditions';

export default function Header( props ) {

const classes = mergeClassNames( 'flex-grow-1', props.className ) + ( ( props._disabled ?? false ) ? ' opacity-50' : '' );
const classes = mergeClassNames( 'flex-grow-1', props.className ) + ( ( isItemDisabled( props ) ?? false ) ? ' opacity-50' : '' );

if ( props.columns ) {
return (
Expand Down
48 changes: 37 additions & 11 deletions assets/react/components/views/Blocks/Actions.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { ButtonGroup, DropdownButton, FormCheck } from 'react-bootstrap';
import { ButtonGroup, DropdownButton, DropdownItem, FormCheck } from 'react-bootstrap';

import useGlobal from '../../../hooks/useGlobal';

import { HStack, VStack } from '../../partials/Stack';
import Button from '../../partials/Button';
import Icon from '../../partials/Icon';
import Label from '../../form/Label';

import Modal from '../../modals/Modal';
import ConfirmModal from '../../modals/ConfirmModal';
import EntityModal from '../../modals/EntityModal';
import ExportModal from '../../modals/ExportModal';
import DeleteModal from '../../modals/DeleteModal';

import Modal from '../../modals/Modal';
import RequestModal from '../../modals/RequestModal';
import PreviewModal from '../../modals/PreviewModal';

import ModalToggle from '../../services/ModalToggle';
import Collapsible from '../../services/Collapsible';
import ResponseTabContent from '../../services/ResponseTabs/Content';
import PreviewModal from '../../modals/PreviewModal';
import { deepClone, objectToMappable } from '../../../utils/data';
import { validate } from '../../../utils/conditions';
import DropdownItem from 'react-bootstrap/DropdownItem';
import Label from '../../form/Label';

function getVariants( button, variant, outline, subtle ) {
const buttonVariant = ( 'string' === typeof button ) ? button : variant;
Expand All @@ -35,7 +36,7 @@ function getVariants( button, variant, outline, subtle ) {
}
}

function createTrigger( action, variants, buttonProps = {}, dropdown = false ) {
function createTrigger( action, variants, elemProps = {}, dropdown = false ) {
let iconClasses = '';

let trigger = action.label ?? action.action;
Expand All @@ -54,9 +55,19 @@ function createTrigger( action, variants, buttonProps = {}, dropdown = false ) {
}
}

const Elemt = dropdown ? DropdownItem : Button;
if ( ! variants.button ) {
return trigger;
}

if ( dropdown ) {
return (
<DropdownItem variant={ variants.button } { ...elemProps } key={ elemProps.key ?? undefined }>
{ trigger }
</DropdownItem>
)
}

return variants.button ? <Elemt variant={ variants.button } outline={ variants.outline } subtle={ variants.subtle } { ...buttonProps } key={ buttonProps.key ?? undefined }>{ trigger }</Elemt> : trigger;
return <Button variant={ variants.button } outline={ variants.outline } subtle={ variants.subtle } { ...elemProps } key={ elemProps.key ?? undefined }>{ trigger }</Button>;
}

export default function Actions( props ) {
Expand Down Expand Up @@ -165,6 +176,7 @@ export default function Actions( props ) {

case 'delete':
case 'remove':
case 'trash':
const deleteVariant = action.variant ?? 'danger';
return (
<DeleteModal key={ action.action } entity={ item } { ...action }>
Expand All @@ -175,14 +187,28 @@ export default function Actions( props ) {
</DeleteModal>
)

case 'restore':
case 'hide':
case 'show':
const trigger = createTrigger( { ...action, icon: action.icon ?? action.action }, variants, {}, action.dropdown );
return React.cloneElement( trigger, { key: action.action, onClick: () => { action.callback( props.entity ?? props.item ?? props.id ) } } );

case 'enable':
case 'disable':
if ( ! item ) {
return null;
}
if ( props.entity || item._entity ) {
return (
<ConfirmModal key={ action.action } entity={ item } { ...action } callbackProps={ item }>
{ createTrigger( { ...action, icon: action.icon ?? action.action }, variants, {}, action.dropdown ) }
</ConfirmModal>
)
}
return (
<FormCheck
key={ key }
aria-label={ t('Disable') }
key={ action.action }
aria-label={ 'disable' === action.action ? t('Disable') : t('Enable') }
className="mt-n1 no-label"
type="switch"
defaultChecked={ ! ( item._disabled ?? false ) }
Expand Down
5 changes: 4 additions & 1 deletion assets/react/components/views/Blocks/Info.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { forwardRef } from 'react';
import { any, bool, object, oneOfType, string } from 'prop-types';
import { any, array, bool, object, oneOfType, string } from 'prop-types';
import { isObject } from '../../../utils/conditions';
import { HStack, VStack } from '../../partials/Stack';
import BadgeControl from './Badge';
Expand All @@ -11,6 +11,7 @@ const Info = forwardRef( function Info( props, ref ) {
item = {},
type = item.type,
badge,
badges,
inline = false,
} = props;

Expand All @@ -25,6 +26,7 @@ const Info = forwardRef( function Info( props, ref ) {
{ badge &&
<BadgeControl type={ type } { ...( isObject( badge ) ? badge : { label: badge } ) } item={ item } />
}
{ badges && badges.map( ( badge, i ) => <BadgeControl key={ i } type={ type } { ...( isObject( badge ) ? badge : { label: badge } ) } item={ item } /> ) }
</HStack>
)

Expand Down Expand Up @@ -52,6 +54,7 @@ Info.propTypes = {
item: object,
type: string,
badge: oneOfType( [ string, object ] ),
badges: array,
label: oneOfType( [ string, object ] ),
sub: oneOfType( [ object, string ] ),
icon: oneOfType( [ object, string, bool ] ),
Expand Down
11 changes: 10 additions & 1 deletion assets/react/components/views/List/Col.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import Block from '../Blocks';
import LoadingPlaceholder from '../../partials/Loading/Placeholder';
import { mergeClassNames } from '../../../utils/props';
import { isItemDisabled } from '../../../utils/conditions';

export default function ListCol( props ) {
const {
Expand All @@ -15,8 +17,14 @@ export default function ListCol( props ) {
return <LoadingPlaceholder/>
}

let classes = '';

if ( isItemDisabled( item ) ) {
classes = 'text-muted';
}

return (
<div className={ column.classes }>
<div className={ mergeClassNames( props.className, classes ) }>
<Block { ...props } block={ blockType } content={ content } args={ column } />
</div>
);
Expand All @@ -27,4 +35,5 @@ ListCol.propTypes = {
item: PropTypes.object,
content: PropTypes.any,
blockType: PropTypes.string,
className: PropTypes.string,
};
4 changes: 3 additions & 1 deletion assets/react/components/views/List/Row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default function ListRow( {
columns,
callbacks,
wrap = true,
className,
} ) {

let row = columns &&
Expand All @@ -32,7 +33,7 @@ export default function ListRow( {
row = <Stack direction="horizontal" className="justify-content-between w-100" gap={2}>{ row }</Stack>

if ( wrap ) {
row = <ListGroupItem>{ row }</ListGroupItem>
row = <ListGroupItem className={ className }>{ row }</ListGroupItem>
}

return row;
Expand All @@ -44,4 +45,5 @@ ListRow.propTypes = {
columns: PropTypes.object,
callbacks: PropTypes.object,
wrap: PropTypes.bool,
className: PropTypes.string,
};
9 changes: 8 additions & 1 deletion assets/react/components/views/Table/Col.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import Block from '../Blocks';
import LoadingPlaceholder from '../../partials/Loading/Placeholder';
import { isItemDisabled } from '../../../utils/conditions';
import { mergeClassNames } from '../../../utils/props';

export default function TableCol( props ) {
const {
Expand All @@ -27,8 +29,12 @@ export default function TableCol( props ) {
break;
}

if ( isItemDisabled( item ) ) {
classes += ' text-muted';
}

return (
<td className={ classes }>
<td className={ mergeClassNames( props.className, classes ) }>
<Block { ...props } block={ blockType } content={ content } args={ column } />
</td>
);
Expand All @@ -39,4 +45,5 @@ TableCol.propTypes = {
item: PropTypes.object,
content: PropTypes.any,
blockType: PropTypes.string,
className: PropTypes.string,
};
4 changes: 3 additions & 1 deletion assets/react/components/views/Table/Row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default function TableRow( {
columns,
callbacks,
wrap = true,
className,
} ) {

let row = objectToMappable( columns, 'key' ).map( ( column, index ) => {
Expand All @@ -28,7 +29,7 @@ export default function TableRow( {
} )

if ( wrap ) {
row = <tr>{ row }</tr>;
row = <tr className={ className }>{ row }</tr>;
}

return row;
Expand All @@ -40,4 +41,5 @@ TableRow.propTypes = {
columns: PropTypes.object,
callbacks: PropTypes.object,
wrap: PropTypes.bool,
className: PropTypes.string,
};
66 changes: 59 additions & 7 deletions assets/react/controllers/ListController.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Card } from 'react-bootstrap';
import { Card, Dropdown, DropdownButton } from 'react-bootstrap';

import Button from '../components/partials/Button';
import useGlobal from '../hooks/useGlobal';
import useEntities from '../hooks/useEntities';
import usePreference from '../hooks/usePreference';
import { HStack } from '../components/partials/Stack';
import Search from '../components/partials/Search';
import Icon from '../components/partials/Icon';
import Label from '../components/form/Label';

import EntityModal from '../components/modals/EntityModal';
import ListView from '../components/views/List';
import TableView from '../components/views/Table';
import Pager from '../components/partials/PaginationToolbar/Pager';
import LoadMore from '../components/partials/PaginationToolbar/LoadMore';
import PaginationInfo from '../components/partials/PaginationToolbar/Info';
import Icon from '../components/partials/Icon';
import { HStack } from '../components/partials/Stack';
import Search from '../components/partials/Search';

import useGlobal from '../hooks/useGlobal';
import useEntities from '../hooks/useEntities';
import usePreference from '../hooks/usePreference';
import { deepClone, objectToMappable } from '../utils/data';
import { isString } from '../utils/conditions';

function getQuery( query ) {
let url = new URL( window.location.href );
Expand Down Expand Up @@ -50,6 +54,7 @@ export default function ListController( props ) {
actions: [ 'pagination' ]
},
columns = {},
filters = [],
} = args;

const disableQuery = false === args.query;
Expand Down Expand Up @@ -81,6 +86,20 @@ export default function ListController( props ) {
toPage: ( pageNumber, pageIndex ) => { itemsCallbacks.fetch( ( query ) => { query.offset = pageIndex * query.limit; return query; } ) },
setLimit: ( limit ) => { itemsCallbacks.fetch( ( query ) => { setPreferredLimit( limit ); query.limit = limit; return query; } ) },
search: ( search, column ) => { itemsCallbacks.fetch( ( query ) => { query.search[ column ?? 'name' ] = search; return query; } ) },
setFilter: ( key, value ) => {
itemsCallbacks.fetch( ( query ) => {
if ( ! query.where ) {
query.where = {};
}

query.where[ key ] = value;

// Always reset to page 1 when filters change.
query.offset = 0;

return query;
});
},
}

/**
Expand Down Expand Up @@ -138,6 +157,39 @@ export default function ListController( props ) {
</HStack>
);

case 'filter':
return (
filters && <HStack key={ action + index } gap={ 2 }>
{ objectToMappable( deepClone( filters ), 'key' ).map( ( filter, i ) => {
const currentFilterValue = query.where?.[ filter.key ] ?? null;
const displayLabel = filter.options.find( opt => opt.value === currentFilterValue )
?? filter.label
?? 'All';

return (
<DropdownButton
key={ i }
id={ `filter-${filter.key}` }
title={ <Label icon={ isString( displayLabel ) ? filter.icon : undefined } label={ displayLabel } /> }
variant="outline-secondary"
drop="down"
className="ms-0"
>
{ filter.options.map( ( opt, j ) => (
<Dropdown.Item
key={ j }
eventKey={ opt.value }
onClick={ () => queryCallbacks.setFilter( filter.key, opt.value ) }
>
<Label { ...opt } />
</Dropdown.Item>
))}
</DropdownButton>
);
})}
</HStack>
);

case 'total':
if ( ! totalItems ) {
return;
Expand Down
Loading
Loading