Skip to content

Commit ebf3e16

Browse files
committed
refactor: replace return undefined with return or return null
- Replace 'return undefined;' with 'return;' in regular functions per style guide - Replace 'return undefined;' with 'return null;' in React components - Keep 'return undefined;' in callbacks where type requires string | undefined - Add PanelBanner component with warning variant to page settings - Show redirect conflict warning in page settings when redirect would override page
1 parent 14cc7f3 commit ebf3e16

File tree

26 files changed

+1066
-123
lines changed

26 files changed

+1066
-123
lines changed

apps/builder/app/builder/features/breakpoints/breakpoint-editor-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const buildBreakpointFromEditorState = (
6666
newBreakpoint.condition = originalBreakpoint.condition;
6767
} else {
6868
// Invalid: no condition and no valid width
69-
return undefined;
69+
return;
7070
}
7171

7272
return newBreakpoint;

apps/builder/app/builder/features/pages/page-settings.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import {
7676
$userPlanFeatures,
7777
$isDesignMode,
7878
} from "~/shared/nano-states";
79+
import { $openProjectSettings } from "~/shared/nano-states/project-settings";
7980
import {
8081
BindingControl,
8182
BindingPopover,
@@ -106,6 +107,7 @@ import {
106107
} from "./page-utils";
107108
import { Form } from "./form";
108109
import { CustomMetadata } from "./custom-metadata";
110+
import { findMatchingRedirect } from "~/shared/project-settings/utils";
109111

110112
const fieldDefaultValues = {
111113
name: "Untitled",
@@ -524,14 +526,24 @@ const LanguageField = ({
524526
);
525527
};
526528

527-
const usePageUrl = (values: Values) => {
528-
const pages = useStore($pages);
529-
const foldersPath =
530-
pages === undefined ? "" : getPagePath(values.parentFolderId, pages);
531-
const path = [foldersPath, values.path]
529+
/**
530+
* Compute the full page path from form values.
531+
* This combines folder path with page path, handling home page special case.
532+
*/
533+
const computePagePath = (values: Values, pages: Pages): string => {
534+
if (values.isHomePage) {
535+
return "/";
536+
}
537+
const foldersPath = getPagePath(values.parentFolderId, pages);
538+
return [foldersPath, values.path]
532539
.filter(Boolean)
533540
.join("/")
534541
.replace(/\/+/g, "/");
542+
};
543+
544+
const usePageUrl = (values: Values) => {
545+
const pages = useStore($pages);
546+
const path = pages === undefined ? "" : computePagePath(values, pages);
535547

536548
const system = useStore($currentSystem);
537549
const publishedOrigin = useStore($publishedOrigin);
@@ -683,9 +695,33 @@ const FormFields = ({
683695
computeExpression(values.excludePageFromSearch, variableValues)
684696
);
685697

698+
// Check if any redirect matches this page's path
699+
const fullPagePath = computePagePath(values, pages);
700+
const matchingRedirect = findMatchingRedirect(
701+
fullPagePath,
702+
pages.redirects ?? []
703+
);
704+
686705
return (
687706
<Grid css={{ height: "100%" }}>
688707
<ScrollArea>
708+
{matchingRedirect && (
709+
<PanelBanner variant="warning">
710+
<Text>
711+
A redirect from "{matchingRedirect.old}" will override this page.
712+
The page will not be rendered when published.{" "}
713+
<Link
714+
color="inherit"
715+
underline="always"
716+
onClick={() => {
717+
$openProjectSettings.set("redirects");
718+
}}
719+
>
720+
Go to Redirects settings
721+
</Link>
722+
</Text>
723+
</PanelBanner>
724+
)}
689725
{/**
690726
* ----------------------========<<<Page props>>>>========----------------------
691727
*/}

apps/builder/app/builder/features/style-panel/sections/backgrounds/background-image.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@ export const BackgroundImage = ({ index }: { index: number }) => {
1616
const newValue = parsed.get("background-image");
1717

1818
if (newValue === undefined || newValue?.type === "invalid") {
19-
return undefined;
19+
return;
2020
}
2121

2222
const [layer] = newValue.type === "layers" ? newValue.value : [newValue];
2323

2424
// Only validate image URLs, not keywords or other types
2525
if (layer?.type !== "image" || layer.value.type !== "url") {
26-
return undefined;
26+
return;
2727
}
2828

2929
const url = layer.value.url;
3030

3131
// If it's an absolute URL, no validation needed
3232
if (isAbsoluteUrl(url)) {
33-
return undefined;
33+
return;
3434
}
3535

3636
// Check if the asset exists in the project
@@ -42,7 +42,7 @@ export const BackgroundImage = ({ index }: { index: number }) => {
4242
return [`Asset ${url} is not found in project`];
4343
}
4444

45-
return undefined;
45+
return;
4646
},
4747
[]
4848
);

apps/builder/app/builder/shared/css-variable-utils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ export const renameCssVariable = (
444444
}
445445
});
446446

447-
return undefined;
447+
return;
448448
};
449449

450450
export const deleteUnusedCssVariables = () => {

apps/builder/app/canvas/features/text-editor/text-editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ const isSelectionFirstNode = () => {
488488
const getDomSelectionRect = () => {
489489
const domSelection = window.getSelection();
490490
if (!domSelection || !domSelection.focusNode) {
491-
return undefined;
491+
return;
492492
}
493493

494494
// Get current line position

apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ const getInstanceSelector = (
282282
return [...selector, ...rootInstanceSelector];
283283
}
284284
}
285-
return undefined;
285+
return;
286286
};
287287

288288
const $indexesWithinAncestors = computed(
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import {
2+
$instances,
3+
$isResizingCanvas,
4+
$stylesIndex,
5+
$propValuesByInstanceSelectorWithMemoryProps,
6+
} from "~/shared/nano-states";
7+
import { $gridCellData, type GridCellData } from "~/shared/nano-states";
8+
import { subscribeScrollState } from "~/canvas/shared/scroll-state";
9+
import { subscribeWindowResize } from "~/shared/dom-hooks";
10+
import type { InstanceSelector } from "~/shared/tree-utils";
11+
import { $awareness } from "~/shared/awareness";
12+
import { getElementByInstanceSelector } from "~/shared/dom-utils";
13+
import { parseGridTemplateTrackList } from "@webstudio-is/css-data";
14+
15+
const hideGridOverlay = () => {
16+
$gridCellData.set(undefined);
17+
};
18+
19+
// Use probe elements to measure exact cell positions
20+
// We probe a grid of positions and dedupe based on actual coordinates
21+
const computeGridCells = (
22+
gridElement: HTMLElement,
23+
instanceId: string
24+
): GridCellData | undefined => {
25+
const computedStyle = window.getComputedStyle(gridElement);
26+
const display = computedStyle.display;
27+
28+
// Only compute for grid containers
29+
if (display !== "grid" && display !== "inline-grid") {
30+
return;
31+
}
32+
33+
const MAX_PROBE = 20; // Reasonable limit for probing
34+
35+
// Get the actual track counts from computed style
36+
// This gives us the explicit tracks without creating implicit ones
37+
const gridTemplateColumns = computedStyle.gridTemplateColumns;
38+
const gridTemplateRows = computedStyle.gridTemplateRows;
39+
40+
// Parse track counts using the proper CSS parser
41+
// Computed values are already expanded (no repeat(), var(), etc.)
42+
const columnTracks = parseGridTemplateTrackList(gridTemplateColumns);
43+
const rowTracks = parseGridTemplateTrackList(gridTemplateRows);
44+
45+
// Ensure at least 1 track, cap at MAX_PROBE
46+
const columnCount = Math.min(Math.max(1, columnTracks.length), MAX_PROBE);
47+
const rowCount = Math.min(Math.max(1, rowTracks.length), MAX_PROBE);
48+
49+
// First, detect actual column and row count by probing positions
50+
// Place probes and track unique X/Y positions
51+
const xPositions = new Set<number>();
52+
const yPositions = new Set<number>();
53+
54+
// Probe all cells to get accurate positions (handles gaps, etc.)
55+
for (let row = 1; row <= rowCount; row++) {
56+
for (let col = 1; col <= columnCount; col++) {
57+
const probe = document.createElement("div");
58+
probe.style.cssText = `
59+
grid-column: ${col};
60+
grid-row: ${row};
61+
pointer-events: none;
62+
visibility: hidden;
63+
align-self: stretch;
64+
justify-self: stretch;
65+
`;
66+
gridElement.appendChild(probe);
67+
const rect = probe.getBoundingClientRect();
68+
gridElement.removeChild(probe);
69+
70+
xPositions.add(rect.left);
71+
xPositions.add(rect.right);
72+
yPositions.add(rect.top);
73+
yPositions.add(rect.bottom);
74+
}
75+
}
76+
77+
// Sort positions
78+
const sortedX = [...xPositions].sort((a, b) => a - b);
79+
const sortedY = [...yPositions].sort((a, b) => a - b);
80+
81+
if (sortedX.length < 2 || sortedY.length < 2) {
82+
return;
83+
}
84+
85+
// Build lines from positions
86+
const gridLeft = sortedX[0];
87+
const gridRight = sortedX[sortedX.length - 1];
88+
const gridTop = sortedY[0];
89+
const gridBottom = sortedY[sortedY.length - 1];
90+
const gridWidth = gridRight - gridLeft;
91+
const gridHeight = gridBottom - gridTop;
92+
93+
const horizontalLines: GridCellData["horizontalLines"] = [];
94+
for (const y of sortedY) {
95+
horizontalLines.push({ y, x: gridLeft, width: gridWidth });
96+
}
97+
98+
const verticalLines: GridCellData["verticalLines"] = [];
99+
for (const x of sortedX) {
100+
verticalLines.push({ x, y: gridTop, height: gridHeight });
101+
}
102+
103+
return {
104+
instanceId,
105+
columnCount,
106+
rowCount,
107+
horizontalLines,
108+
verticalLines,
109+
};
110+
};
111+
112+
const findGridContainer = (
113+
instanceSelector: Readonly<InstanceSelector>
114+
): { element: HTMLElement; instanceId: string } | undefined => {
115+
// Check each ancestor in the selector
116+
for (let i = 0; i < instanceSelector.length; i++) {
117+
const ancestorSelector = instanceSelector.slice(i);
118+
const element = getElementByInstanceSelector(ancestorSelector);
119+
120+
if (element) {
121+
const computedStyle = window.getComputedStyle(element);
122+
const display = computedStyle.display;
123+
124+
if (display === "grid" || display === "inline-grid") {
125+
return { element, instanceId: instanceSelector[i] };
126+
}
127+
}
128+
}
129+
130+
return;
131+
};
132+
133+
const subscribeGridOverlay = (
134+
selectedInstanceSelector: Readonly<InstanceSelector>,
135+
debounceEffect: (callback: () => void) => void
136+
) => {
137+
if (selectedInstanceSelector.length === 0) {
138+
hideGridOverlay();
139+
return;
140+
}
141+
142+
const gridInfo = findGridContainer(selectedInstanceSelector);
143+
144+
if (!gridInfo) {
145+
hideGridOverlay();
146+
return;
147+
}
148+
149+
const { element: gridElement, instanceId } = gridInfo;
150+
151+
const updateGridOverlay = () => {
152+
if ($isResizingCanvas.get()) {
153+
hideGridOverlay();
154+
return;
155+
}
156+
157+
const cellData = computeGridCells(gridElement, instanceId);
158+
$gridCellData.set(cellData);
159+
};
160+
161+
// Initial computation
162+
updateGridOverlay();
163+
164+
const unsubscribeStylesIndex = $stylesIndex.subscribe(() => {
165+
// Styles are applied to DOM via RAF in the stylesheet renderer,
166+
// then browser needs another frame to recalculate layout
167+
requestAnimationFrame(() => {
168+
requestAnimationFrame(() => {
169+
debounceEffect(updateGridOverlay);
170+
});
171+
});
172+
});
173+
174+
const unsubscribeInstances = $instances.subscribe(() => {
175+
debounceEffect(updateGridOverlay);
176+
});
177+
178+
const unsubscribePropValues =
179+
$propValuesByInstanceSelectorWithMemoryProps.subscribe(() => {
180+
debounceEffect(updateGridOverlay);
181+
});
182+
183+
const unsubscribeIsResizing = $isResizingCanvas.subscribe((isResizing) => {
184+
if (isResizing) {
185+
hideGridOverlay();
186+
} else {
187+
updateGridOverlay();
188+
}
189+
});
190+
191+
const unsubscribeScrollState = subscribeScrollState({
192+
onScrollStart: hideGridOverlay,
193+
onScrollEnd: updateGridOverlay,
194+
});
195+
196+
const unsubscribeWindowResize = subscribeWindowResize({
197+
onResizeStart: hideGridOverlay,
198+
onResizeEnd: updateGridOverlay,
199+
});
200+
201+
return () => {
202+
hideGridOverlay();
203+
unsubscribeStylesIndex();
204+
unsubscribeInstances();
205+
unsubscribePropValues();
206+
unsubscribeIsResizing();
207+
unsubscribeScrollState();
208+
unsubscribeWindowResize();
209+
};
210+
};
211+
212+
export const subscribeGridOverlayOnSelected = (
213+
debounceEffect: (callback: () => void) => void
214+
) => {
215+
let previousSelectedInstance: readonly string[] | undefined = undefined;
216+
let unsubscribeGridOverlay = () => {};
217+
218+
const unsubscribe = $awareness.subscribe((awareness) => {
219+
const instanceSelector = awareness?.instanceSelector;
220+
if (instanceSelector !== previousSelectedInstance) {
221+
unsubscribeGridOverlay();
222+
unsubscribeGridOverlay =
223+
subscribeGridOverlay(instanceSelector ?? [], debounceEffect) ??
224+
(() => {});
225+
previousSelectedInstance = instanceSelector;
226+
}
227+
});
228+
229+
return () => {
230+
unsubscribe();
231+
unsubscribeGridOverlay();
232+
};
233+
};

0 commit comments

Comments
 (0)