Skip to content
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 page parameters to navigation actions #1876

Merged
merged 33 commits into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7bb9ca0
Add page parameters to navigation actions
apedroferreira Apr 12, 2023
b1f0489
Non-working bindings for actions + fix circular dependency hell
apedroferreira Apr 12, 2023
c351057
where did this even come from
apedroferreira Apr 12, 2023
99bb4a3
Fix types
apedroferreira Apr 12, 2023
d8c8c0b
Working page parameter bindings
apedroferreira Apr 13, 2023
2007d59
Fix and add test
apedroferreira Apr 13, 2023
005f5d1
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 13, 2023
31d818a
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 14, 2023
2d7dcdc
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 20, 2023
07c2cee
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 21, 2023
4d738a0
Fixes for rebase + new page file structure
apedroferreira Apr 21, 2023
30c2075
hehe
apedroferreira Apr 21, 2023
8bcf041
Remove unneeded fixture things
apedroferreira Apr 21, 2023
13bcc23
Fix page name
apedroferreira Apr 21, 2023
d28474c
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 21, 2023
e6d0e88
Improve test for navigation overall + see if CI passes
apedroferreira Apr 21, 2023
80a4dfe
Better test
apedroferreira Apr 21, 2023
8e4fec1
Attempt to get CI to pass again
apedroferreira Apr 21, 2023
1fcd96e
Remove check to try to debug issue in CI
apedroferreira Apr 21, 2023
1e44bbb
Revert "Remove check to try to debug issue in CI"
apedroferreira Apr 21, 2023
e68d8e0
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 24, 2023
c1fe290
Another attempt to make CI pass
apedroferreira Apr 24, 2023
4185ed8
Try again
apedroferreira Apr 24, 2023
f8d176d
Try using different method
apedroferreira Apr 24, 2023
e2ce209
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 26, 2023
e057975
Use $ref instead of $$ref everywhere (mystery solved?)
apedroferreira Apr 26, 2023
c246b3d
Forgot this one
apedroferreira Apr 27, 2023
55cd8ba
Refactor with ignored cycle to see what changed
apedroferreira Apr 27, 2023
ef1980a
Revert some more changes
apedroferreira Apr 27, 2023
4e1c55c
fix lint
apedroferreira Apr 27, 2023
6ff80f1
Merge remote-tracking branch 'origin/master' into add-navigation-page…
apedroferreira Apr 27, 2023
3d255de
Document navigation actions
apedroferreira Apr 28, 2023
558648b
Allow navigating to same page
apedroferreira Apr 28, 2023
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
4 changes: 2 additions & 2 deletions packages/toolpad-app/src/appDom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1114,15 +1114,15 @@ export function ref(nodeId: NodeId): NodeReference;
export function ref(nodeId: null | undefined): null;
export function ref(nodeId: Maybe<NodeId>): NodeReference | null;
export function ref(nodeId: Maybe<NodeId>): NodeReference | null {
return nodeId ? { $$ref: nodeId } : null;
return nodeId ? { $ref: nodeId } : null;
Copy link
Member Author

@apedroferreira apedroferreira Apr 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we inject the initial DOM state in the HTML in the <script> tags all the $$ref properties are being changed to $ref now with the Vite runtime... Not sure exactly why.
So all navigation actions were broken.
Can we just use $ref for these properties? It shouldn't even need a migration, right? I only saw it being used in a few old connections besides navigation actions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently have two formats for bindings/actions/...

One legacy one in the appDom, one new one from the new file system format. Over time we will move the whole application over to the second format

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! How do you suggest fixing the navigation actions for now? appDom.deRef tries to get $$ref but the local DOM has the page as $ref.
Are the changes in this PR about that ok or do you suggest an alternative?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you're changing the existing format, my suggestion would be to change it to align it close to the what's in the file schema. The more we can reduce the amount of conversion needed from schema to internal representation, the better. With the north star goal to get rid of the conversion completely.
Otherwise, if it works, it works, and this can be merged

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot this comment... will take a second look and add changes if it makes sense.

}

export function deref(nodeRef: NodeReference): NodeId;
export function deref(nodeRef: null | undefined): null;
export function deref(nodeRef: Maybe<NodeReference>): NodeId | null;
export function deref(nodeRef: Maybe<NodeReference>): NodeId | null {
if (nodeRef) {
return nodeRef.$$ref;
return nodeRef.$ref;
}
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/src/runtime/CanvasHooksContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';
import { NodeHashes } from '../types';

export interface NavigateToPage {
(pageNodeId: NodeId): void;
(pageNodeId: NodeId, pageParameters?: Record<string, string>): void;
}

/**
Expand Down
42 changes: 35 additions & 7 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,17 @@ const USE_DATA_QUERY_CONFIG_KEYS: readonly (keyof UseDataQueryConfig)[] = [
function usePageNavigator(): NavigateToPage {
const navigate = useNavigate();
const navigateToPage: NavigateToPage = React.useCallback(
(pageNodeId: NodeId) => {
navigate(`/pages/${pageNodeId}`);
(pageNodeId, pageParameters) => {
const urlParams = pageParameters && new URLSearchParams(pageParameters);

navigate({
pathname: `/pages/${pageNodeId}`,
...(urlParams
? {
search: urlParams.toString(),
}
: {}),
});
},
[navigate],
);
Expand Down Expand Up @@ -345,10 +354,28 @@ function RenderedNodeContent({ node, childNodeGroups, Component }: RenderedNodeC
const action = (node as appDom.ElementNode).props?.[key];

if (action?.type === 'navigationAction') {
const handler = () => {
const { page } = action.value;
const handler = async () => {
const { page, parameters = {} } = action.value;
if (page) {
navigateToPage(appDom.deref(page));
const parsedParameterEntries = await Promise.all(
Object.keys(parameters).map(async (parameterName) => {
const parameterValue = parameters[parameterName];

if (parameterValue && parameterValue.type === 'jsExpression') {
const result = await evaluatePageExpression(
parameterValue.value,
scopeId,
localScopeParams,
);
return [parameterName, result.value];
}
return [parameterName, parameterValue?.value];
}),
);

const parsedParameters = Object.fromEntries(parsedParameterEntries);

navigateToPage(appDom.deref(page), parsedParameters);
}
};

Expand Down Expand Up @@ -736,9 +763,10 @@ function parseBindings(
? `${elm.id}.props.${argType.defaultValueProp}`
: undefined;

const propValue = elm.props?.[propName];

const binding: BindableAttrValue<any> =
elm.props?.[propName] ||
appDom.createConst(argType ? getArgTypeDefaultValue(argType) : undefined);
propValue || appDom.createConst(argType ? getArgTypeDefaultValue(argType) : undefined);

const bindingId = `${elm.id}.props.${propName}`;

Expand Down
6 changes: 3 additions & 3 deletions packages/toolpad-app/src/server/localMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,12 @@ function toBindableProp<V>(value: V | { $$jsExpression: string }): BindableAttrV
if (typeof (value as any).$$jsExpressionAction === 'string') {
return { type: 'jsExpressionAction', value: (value as any).$$jsExpressionAction };
}
if (typeof (value as any).$$navigationAction === 'string') {
if (typeof (value as any).$$navigationAction === 'object') {
const action = value as any as NavigationAction;
return {
type: 'navigationAction',
value: {
page: { $$ref: action.$$navigationAction.page as NodeId },
page: { $ref: action.$$navigationAction.page as NodeId },
parameters: mapValues(
action.$$navigationAction.parameters,
(param) => param && toBindable(param),
Expand All @@ -438,7 +438,7 @@ function fromBindableProp<V>(bindable: BindableAttrValue<V>) {
case 'navigationAction':
return {
$$navigationAction: {
page: bindable.value.page.$$ref,
page: bindable.value.page.$ref,
parameters:
bindable.value.parameters &&
mapValues(bindable.value.parameters, (param) => param && fromBindable(param)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TabContext, TabList } from '@mui/lab';
import { Box, Tab } from '@mui/material';
import * as React from 'react';
import { BindableAttrValue } from '@mui/toolpad-core';
import { NavigationActionEditor } from './NavigationActionEditor';
import TabPanel from '../../../components/TabPanel';
import { Maybe, WithControlledProp } from '../../../utils/types';
import { BindableType, JsExpressionActionEditor } from '.';

function getActionTab(value: Maybe<BindableAttrValue<any>>) {
return value?.type || 'jsExpressionAction';
}

export interface ActionEditorProps extends WithControlledProp<BindableAttrValue<any> | null> {}

export function ActionEditor({ value, onChange }: ActionEditorProps) {
const [activeTab, setActiveTab] = React.useState<BindableType>(getActionTab(value));
React.useEffect(() => setActiveTab(getActionTab(value)), [value]);

const handleTabChange = (event: React.SyntheticEvent, newValue: BindableType) => {
setActiveTab(newValue);
};

return (
<Box>
<TabContext value={activeTab}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<TabList onChange={handleTabChange} aria-label="Choose action kind ">
<Tab label="JS expression" value="jsExpressionAction" />
<Tab label="Navigation" value="navigationAction" />
</TabList>
</Box>
<TabPanel value="jsExpressionAction" disableGutters>
<JsExpressionActionEditor
value={value?.type === 'jsExpressionAction' ? value : null}
onChange={onChange}
/>
</TabPanel>
<TabPanel value="navigationAction" disableGutters>
<NavigationActionEditor
value={value?.type === 'navigationAction' ? value : null}
onChange={onChange}
/>
</TabPanel>
</TabContext>
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
import * as React from 'react';
import { BindableAttrValue, PropValueType } from '@mui/toolpad-core';

import { tryFormatExpression } from '../../../utils/prettier';
import useShortcut from '../../../utils/useShortcut';
import useUnsavedChangesConfirm from '../../hooks/useUnsavedChangesConfirm';
import { WithControlledProp } from '../../../utils/types';
import { JsBindingEditor, useBindingEditorContext } from '.';

export interface BindingEditorDialogProps<V>
extends WithControlledProp<BindableAttrValue<V> | null> {
open: boolean;
onClose: () => void;
renderPropBindingEditor?: (
propType: PropValueType,
controlProps: WithControlledProp<BindableAttrValue<V> | null>,
) => JSX.Element | null;
}

export function BindingEditorDialog<V>({
value,
onChange,
open,
onClose,
renderPropBindingEditor,
}: BindingEditorDialogProps<V>) {
const { propType, label } = useBindingEditorContext();

const [input, setInput] = React.useState(value);
React.useEffect(() => {
if (open) {
setInput(value);
}
}, [open, value]);

const committedInput = React.useRef<BindableAttrValue<V> | null>(input);

const handleSave = React.useCallback(() => {
let newValue = input;

if (input?.type === 'jsExpression') {
newValue = {
...input,
value: tryFormatExpression(input.value),
};
}

committedInput.current = newValue;
onChange(newValue);
}, [onChange, input]);

const hasUnsavedChanges = input
? input.type !== committedInput.current?.type || input.value !== committedInput.current?.value
: false;

const { handleCloseWithUnsavedChanges } = useUnsavedChangesConfirm({
hasUnsavedChanges,
onClose,
});

const handleCommit = React.useCallback(() => {
handleSave();
onClose();
}, [onClose, handleSave]);

const handleRemove = React.useCallback(() => {
onChange(null);
onClose();
}, [onClose, onChange]);

useShortcut({ key: 's', metaKey: true, disabled: !open }, handleSave);

const propBindingEditor =
renderPropBindingEditor &&
propType &&
renderPropBindingEditor(propType, {
value: input,
onChange: (newValue) => setInput(newValue),
});

return (
<Dialog
onClose={handleCloseWithUnsavedChanges}
open={open}
fullWidth
scroll="body"
maxWidth="lg"
>
<DialogTitle>Bind property &quot;{label}&quot;</DialogTitle>
<DialogContent>
{propBindingEditor || (
<JsBindingEditor
value={input?.type === 'jsExpression' ? input : null}
onChange={(newValue) => setInput(newValue)}
/>
)}
</DialogContent>
<DialogActions>
<Button color="inherit" variant="text" onClick={onClose}>
{hasUnsavedChanges ? 'Cancel' : 'Close'}
</Button>
<Button color="inherit" disabled={!value} onClick={handleRemove}>
Remove binding
</Button>
<Button disabled={!hasUnsavedChanges} color="primary" onClick={handleCommit}>
Update binding
</Button>
</DialogActions>
</Dialog>
);
}
Loading