Skip to content

Commit

Permalink
Guess proper default page titles based on page name (#3014)
Browse files Browse the repository at this point in the history
  • Loading branch information
Janpot authored Dec 21, 2023
1 parent d4e88b3 commit 3d83c77
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 87 deletions.
8 changes: 6 additions & 2 deletions packages/toolpad-app/src/appDom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@mui/toolpad-core';
import invariant from 'invariant';
import { BoxProps, ThemeOptions as MuiThemeOptions } from '@mui/material';
import { pascalCase, removeDiacritics, uncapitalize } from '@mui/toolpad-utils/strings';
import { guessTitle, pascalCase, removeDiacritics, uncapitalize } from '@mui/toolpad-utils/strings';
import { mapProperties, mapValues, hasOwnProperty } from '@mui/toolpad-utils/collections';
import { ConnectionStatus } from '../types';
import { omit, update, updateOrCreate } from '../utils/immutability';
Expand Down Expand Up @@ -1206,8 +1206,12 @@ export function getRequiredEnvVars(dom: AppDom): Set<string> {
return new Set(allVars);
}

export function getPageDisplayName(node: PageNode): string {
return node.attributes.displayName || guessTitle(node.name);
}

export function getPageTitle(node: PageNode): string {
return node.attributes.title || node.name;
return node.attributes.title || getPageDisplayName(node);
}

export function isCodePage(node: PageNode): boolean {
Expand Down
2 changes: 1 addition & 1 deletion packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1560,7 +1560,7 @@ function ToolpadAppLayout({ dom }: ToolpadAppLayoutProps) {
() =>
pages.map((page) => ({
slug: page.name,
displayName: page.attributes.displayName ?? page.name,
displayName: appDom.getPageDisplayName(page),
hasShell: page?.attributes.display !== 'standalone',
})),
[pages],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { TextField } from '@mui/material';
import { IconButton, InputAdornment, TextField, Tooltip } from '@mui/material';
import * as React from 'react';
import { AppDom, PageNode, setNodeNamespacedProp } from '../../appDom';
import ResetIcon from '@mui/icons-material/RestartAlt';
import * as appDom from '../../appDom';
import { useDomApi } from '../AppState';

interface PageDisplayNameEditorProps {
node: PageNode;
node: appDom.PageNode;
}

function validateInput(input: string) {
Expand All @@ -16,21 +17,29 @@ function validateInput(input: string) {

export default function PageDisplayNameEditor({ node }: PageDisplayNameEditorProps) {
const domApi = useDomApi();
const [pageDisplayNameInput, setPageDisplayNameInput] = React.useState(
node.attributes.displayName ?? node.name,
);

const pageDisplayName = React.useMemo(() => appDom.getPageDisplayName(node), [node]);

const [pageDisplayNameInput, setPageDisplayNameInput] = React.useState(pageDisplayName);
React.useEffect(() => setPageDisplayNameInput(pageDisplayName), [pageDisplayName]);

const handlePageDisplayNameChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => setPageDisplayNameInput(event.target.value),
[],
);

const handleCommit = React.useCallback(() => {
domApi.update((dom: AppDom) =>
setNodeNamespacedProp(dom, node, 'attributes', 'displayName', pageDisplayNameInput),
domApi.update((dom: appDom.AppDom) =>
appDom.setNodeNamespacedProp(dom, node, 'attributes', 'displayName', pageDisplayNameInput),
);
}, [node, pageDisplayNameInput, domApi]);

const handleReset = React.useCallback(() => {
domApi.update((dom: appDom.AppDom) =>
appDom.setNodeNamespacedProp(dom, node, 'attributes', 'displayName', undefined),
);
}, [node, domApi]);

const handleKeyPress = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Enter') {
Expand All @@ -50,6 +59,18 @@ export default function PageDisplayNameEditor({ node }: PageDisplayNameEditorPro
onKeyDown={handleKeyPress}
error={!pageDisplayNameInput}
helperText={validateInput(pageDisplayNameInput)}
InputProps={{
endAdornment:
pageDisplayNameInput === node.attributes.displayName ? (
<InputAdornment position="end">
<Tooltip title="Reset to default value">
<IconButton onClick={handleReset} edge="end">
<ResetIcon />
</IconButton>
</Tooltip>
</InputAdornment>
) : null,
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,8 @@ export default function PagesExplorer({ className }: PagesExplorerProps) {
key={page.id}
nodeId={page.id}
toolpadNodeId={page.id}
labelText={page.name}
labelText={appDom.getPageDisplayName(page)}
title={page.name}
onRenameNode={handleRenameNode}
onDuplicateNode={handleDuplicateNode}
onDeleteNode={handleDeletePage}
Expand Down
65 changes: 0 additions & 65 deletions packages/toolpad-app/src/toolpad/ToolpadHomeShell.tsx

This file was deleted.

4 changes: 3 additions & 1 deletion packages/toolpad-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@
"invariant": "2.2.4",
"prettier": "2.8.8",
"react-is": "18.2.0",
"title": "3.5.3",
"yaml": "2.3.4",
"yaml-diff-patch": "2.0.0"
},
"devDependencies": {
"@types/invariant": "2.2.37",
"@types/prettier": "2.7.3",
"@types/react": "18.2.45",
"@types/react-is": "18.2.4"
"@types/react-is": "18.2.4",
"@types/title": "3.4.3"
}
}
25 changes: 24 additions & 1 deletion packages/toolpad-utils/src/strings.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, test, expect } from 'vitest';
import { findImports, capitalize, uncapitalize, pascalCase, camelCase } from './strings';
import {
findImports,
capitalize,
uncapitalize,
pascalCase,
camelCase,
guessTitle,
} from './strings';

describe('findImports', () => {
test('finds all imports', () => {
Expand Down Expand Up @@ -87,3 +94,19 @@ describe('camelCase', () => {
expect(camelCase(...got)).toEqual(expected);
});
});

describe('guessTitle', () => {
test.each([
['camelCaseExample', 'Camel Case Example'],
['snake_case_example', 'Snake Case Example'],
['kebab-case-example', 'Kebab Case Example'],
['ACRONYMExample', 'Acronym Example'],
['helloACRONYMExample', 'Hello Acronym Example'],
['HelloACRONYMExample', 'Hello Acronym Example'],
['example123', 'Example 123'],
['example123Wat', 'Example 123 Wat'],
['example123more456', 'Example 123 More 456'],
])('should split %p into %p', (got, expected) => {
expect(guessTitle(got)).toEqual(expected);
});
});
16 changes: 16 additions & 0 deletions packages/toolpad-utils/src/strings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import title from 'title';

/**
* Makes the first letter of [str] uppercase.
* Not locale aware.
Expand Down Expand Up @@ -171,3 +173,17 @@ export function indent(text: string, length = 2): string {
export function isValidJsIdentifier(base: string): boolean {
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(base);
}

export function guessTitle(str: string): string {
// Replace snake_case with space
str = str.replace(/[_-]/g, ' ');
// Split camelCase
str = str.replace(/([a-z0-9])([A-Z])/g, '$1 $2');
// Split acronyms
str = str.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
// Split numbers
str = str.replace(/([a-zA-Z])(\d+)/g, '$1 $2');
str = str.replace(/(\d+)([a-zA-Z])/g, '$1 $2');

return title(str);
}
6 changes: 3 additions & 3 deletions test/integration/duplication/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test('duplication', async ({ page }) => {
await editorModel.goto();

{
await editorModel.openPageExplorerMenu('page1');
await editorModel.openPageExplorerMenu('Page 1');
const duplicateMenuItem = page.getByRole('menuitem', { name: 'Duplicate' });
await duplicateMenuItem.click();

Expand All @@ -28,12 +28,12 @@ test('duplication', async ({ page }) => {
const button = editorModel.appCanvas.getByRole('button', { name: 'hello world' });
await expect(button).toBeVisible();

await editorModel.openPageExplorerMenu('page2');
await editorModel.openPageExplorerMenu('Page 2');
const deleteMenuItem = page.getByRole('menuitem', { name: 'Delete' });
await deleteMenuItem.click();
const deleteButton = editorModel.confirmationDialog.getByRole('button', { name: 'Delete' });
await deleteButton.click();

await expect(editorModel.getExplorerItem('page2')).toBeHidden();
await expect(editorModel.getExplorerItem('Page 2')).toBeHidden();
}
});
2 changes: 1 addition & 1 deletion test/integration/editor/new.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ test('can create/delete page', async ({ page, localApp }) => {

await editorModel.createPage('someOtherPage');

const pageMenuItem = editorModel.getExplorerItem('someOtherPage');
const pageMenuItem = editorModel.getExplorerItem('Some Other Page');
const pageFolder = path.resolve(localApp.dir, './toolpad/pages/someOtherPage');
const pageFile = path.resolve(pageFolder, './page.yml');

Expand Down
2 changes: 1 addition & 1 deletion test/integration/pages/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ test('can rename page', async ({ page, localApp }) => {
const oldPageFolder = path.resolve(localApp.dir, './toolpad/pages/page2');
await expect.poll(async () => folderExists(oldPageFolder)).toBe(true);

await editorModel.explorer.getByText('page2').dblclick();
await editorModel.explorer.getByText('Page 2').dblclick();
await page.keyboard.type('renamedpage');
await page.keyboard.press('Enter');

Expand Down
2 changes: 1 addition & 1 deletion test/integration/undo-redo/multiple-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test('test undo and redo through different pages', async ({ page }) => {
});
await expect(pageButton1).toBeVisible();

await editorModel.explorer.getByText('page2').click();
await editorModel.explorer.getByText('Page 2').click();

const pageButton2 = editorModel.appCanvas.getByRole('button', {
name: 'page2Button',
Expand Down
Loading

0 comments on commit 3d83c77

Please sign in to comment.