Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ import {
$registeredComponentMetas,
} from "~/shared/nano-states";
import { isRichText } from "~/shared/content-model";
import { $selectedInstancePath } from "~/shared/awareness";
import { $selectedInstance, $selectedInstancePath } from "~/shared/awareness";
import {
$selectedInstanceInitialPropNames,
$selectedInstancePropsMetas,
showAttributeMeta,
type PropValue,
} from "../shared";
import { $instanceTags } from "../../style-panel/shared/model";

type PropOrName = { prop?: Prop; propName: string };

Expand Down Expand Up @@ -157,32 +158,41 @@ const $canHaveTextContent = computed(
}
);

const contentModePropertiesByTag: Partial<Record<string, string[]>> = {
img: ["src", "width", "height", "alt"],
a: ["href"],
};

const $selectedInstanceTag = computed(
[$selectedInstance, $instanceTags],
(selectedInstance, instanceTags) => {
if (selectedInstance === undefined) {
return;
}
return instanceTags.get(selectedInstance.id);
}
);

/** usePropsLogic expects that key={instanceId} is used on the ancestor component */
export const usePropsLogic = ({
instance,
props,
updateProp,
}: UsePropsLogicInput) => {
const isContentMode = useStore($isContentMode);
const selectedInstanceTag = useStore($selectedInstanceTag);

/**
* In content edit mode we show only Image and Link props
* In the future I hope the only thing we will show will be Components
*/
const isPropVisible = (propName: string) => {
const contentModeWhiteList: Partial<Record<string, string[]>> = {
Image: ["src", "width", "height", "alt"],
Link: ["href"],
RichTextLink: ["href"],
};

if (!isContentMode) {
return true;
}

const propsWhiteList = contentModeWhiteList[instance.component] ?? [];

return propsWhiteList.includes(propName);
const allowedProperties =
contentModePropertiesByTag[selectedInstanceTag ?? ""] ?? [];
return allowedProperties.includes(propName);
};

const savedProps = props;
Expand Down Expand Up @@ -231,11 +241,7 @@ export const usePropsLogic = ({
const initialProps: PropAndMeta[] = [];
for (const name of initialPropNames) {
const propMeta = propsMetas.get(name);

if (propMeta === undefined) {
console.error(
`The prop "${name}" is defined in meta.initialProps but not in meta.props`
);
continue;
}

Expand Down
91 changes: 49 additions & 42 deletions apps/builder/app/builder/features/settings-panel/resource-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,17 @@ import {
type InstancePath,
} from "~/shared/awareness";
import { updateWebstudioData } from "~/shared/instance-utils";
import {
computeExpression,
rebindTreeVariablesMutable,
} from "~/shared/data-variables";
import { rebindTreeVariablesMutable } from "~/shared/data-variables";
import { parseCurl, type CurlRequest } from "./curl";

export const parseResource = ({
id,
control,
name,
formData,
}: {
id: string;
control?: string;
name?: string;
formData: FormData;
}) => {
Expand All @@ -87,6 +86,7 @@ export const parseResource = ({
const headerValues = formData.getAll("header-value") as string[];
return Resource.parse({
id,
control,
name: name ?? formData.get("name"),
url: formData.get("url"),
searchParams: searchParamNames
Expand Down Expand Up @@ -253,7 +253,7 @@ const SearchParamPair = ({
}) => {
const evaluatedValue = evaluateExpressionWithinScope(value, scope);
// expressions with variables or objects cannot be edited from input
const isValueUnbound =
const isValueUnboundString =
isLiteralExpression(value) && typeof evaluatedValue === "string";
return (
<Grid
Expand All @@ -274,7 +274,7 @@ const SearchParamPair = ({
<InputField
placeholder="Value"
name="search-param-value-literal"
disabled={!isValueUnbound}
disabled={!isValueUnboundString}
value={serializeValue(evaluatedValue)}
// update text value as string literal
onChange={(event) =>
Expand All @@ -284,7 +284,7 @@ const SearchParamPair = ({
<BindingPopover
scope={scope}
aliases={aliases}
variant={isValueUnbound ? "default" : "bound"}
variant={isLiteralExpression(value) ? "default" : "bound"}
value={value}
onChange={(newValue) => onChange(name, newValue)}
onRemove={(evaluatedValue) =>
Expand Down Expand Up @@ -377,7 +377,7 @@ const HeaderPair = ({
}) => {
const evaluatedValue = evaluateExpressionWithinScope(value, scope);
// expressions with variables or objects cannot be edited from input
const isValueUnbound =
const isValueUnboundString =
isLiteralExpression(value) && typeof evaluatedValue === "string";
return (
<Grid
Expand All @@ -398,7 +398,7 @@ const HeaderPair = ({
<InputField
placeholder="Value"
name="header-value-validator"
disabled={!isValueUnbound}
disabled={!isValueUnboundString}
value={serializeValue(evaluatedValue)}
// update text value as string literal
onChange={(event) =>
Expand All @@ -408,7 +408,7 @@ const HeaderPair = ({
<BindingPopover
scope={scope}
aliases={aliases}
variant={isValueUnbound ? "default" : "bound"}
variant={isLiteralExpression(value) ? "default" : "bound"}
value={value}
onChange={(newValue) => onChange(name, newValue)}
onRemove={(evaluatedValue) =>
Expand Down Expand Up @@ -558,7 +558,7 @@ export const getResourceScopeForInstance = ({
const name = encodeDataVariableId(dataSourceId);
scope[name] = value;
aliases.set(name, dataSource.name);
variableValues.set(dataSource.name, value);
variableValues.set(dataSourceId, value);
}
}
}
Expand Down Expand Up @@ -621,7 +621,7 @@ export const useResourceScope = ({ variable }: { variable?: DataSource }) => {
const key = encodeDataVariableId(variable.id);
delete newScope[key];
newAliases.delete(key);
newVariableValues.delete(variable.name);
newVariableValues.delete(variable.id);
}
return {
scope: newScope,
Expand Down Expand Up @@ -796,7 +796,10 @@ const parseHeaders = (headers: Resource["headers"]) => {
let maxAge: undefined | string;
let bodyType: BodyType;
const newHeaders = headers.filter((header) => {
const value = computeExpression(header.value, new Map()).toLowerCase();
// cast raw expression result to string
const value = String(
evaluateExpressionWithinScope(header.value, {})
).toLowerCase();
if (isCacheControl(header.name)) {
// move simple header like Cache-Control: max-age=10 to dedicated input
// preserve more complex cache-control
Expand All @@ -818,7 +821,7 @@ const parseHeaders = (headers: Resource["headers"]) => {
return false;
}
}
return false;
return true;
});
return { headers: newHeaders, maxAge, bodyType };
};
Expand Down Expand Up @@ -980,8 +983,6 @@ export const SystemResourceForm = forwardRef<
? resources.get(variable.resourceId)
: undefined;

const method = "get";

const localResources = [
{
label: "Sitemap",
Expand All @@ -1006,19 +1007,15 @@ export const SystemResourceForm = forwardRef<
if (scopeInstanceId === undefined) {
return;
}
const name = z.string().parse(formData.get("name"));
const newResource: Resource = {
const newResource: Resource = parseResource({
id: resource?.id ?? nanoid(),
name,
control: "system",
url: localResource.value,
method,
headers: [],
};
formData,
});
const newVariable: DataSource = {
id: variable?.id ?? nanoid(),
scopeInstanceId,
name,
name: newResource.name,
type: "resource",
resourceId: newResource.id,
};
Expand All @@ -1037,6 +1034,8 @@ export const SystemResourceForm = forwardRef<

return (
<>
<input type="hidden" name="method" value="get" />
<input type="hidden" name="url" value={localResource.value} />
<Flex direction="column" css={{ gap: theme.spacing[3] }}>
<Label htmlFor={resourceId}>Resource</Label>
<Select
Expand Down Expand Up @@ -1116,29 +1115,15 @@ export const GraphqlResourceForm = forwardRef<
if (scopeInstanceId === undefined) {
return;
}
const name = z.string().parse(formData.get("name"));
const body = generateObjectExpression(
new Map([
["query", JSON.stringify(query)],
["variables", variables],
])
);
const newResource: Resource = {
const newResource = parseResource({
id: resource?.id ?? nanoid(),
name,
control: "graphql",
url,
method: "post",
headers: [
...headers,
{ name: "Content-Type", value: "application/json" },
],
body,
};
formData,
});
const newVariable: DataSource = {
id: variable?.id ?? nanoid(),
scopeInstanceId,
name,
name: newResource.name,
type: "resource",
resourceId: newResource.id,
};
Expand All @@ -1155,6 +1140,28 @@ export const GraphqlResourceForm = forwardRef<

return (
<>
<input type="hidden" name="method" value="post" />
{!headers.some(({ name }) => isContentType(name)) && (
<>
<input type="hidden" name="header-name" value="Content-Type" />
<input
type="hidden"
name="header-value"
value={`"application/json"`}
/>
</>
)}
<input
type="hidden"
name="body"
value={generateObjectExpression(
new Map([
["query", JSON.stringify(query)],
["variables", variables],
])
)}
/>

<UrlField
scope={scope}
aliases={aliases}
Expand Down
10 changes: 9 additions & 1 deletion apps/builder/app/builder/shared/sync/sync-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,18 @@ const syncServer = async function () {
if (details.authToken) {
headers.append("x-auth-token", details.authToken);
}
// revise patches are not used on the server and reduce possible patch size
const optimizedTransactions = transactions.map((transaction) => ({
...transaction,
payload: transaction.payload.map((change) => ({
namespace: change.namespace,
patches: change.patches,
})),
}));
const response = await fetch(restPatchPath(), {
method: "post",
body: JSON.stringify({
transactions,
transactions: optimizedTransactions,
buildId: details.buildId,
projectId,
// provide latest stored version to server
Expand Down
8 changes: 6 additions & 2 deletions apps/builder/app/routes/rest.patch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { applyPatches, enableMapSet, enablePatches } from "immer";
import { applyPatches, enableMapSet, enablePatches, type Patch } from "immer";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import type { Change } from "immerhin";
import {
Breakpoints,
Breakpoint,
Expand Down Expand Up @@ -51,6 +50,11 @@ import { preventCrossOriginCookie } from "~/services/no-cross-origin-cookie";
import { checkCsrf } from "~/services/csrf-session.server";
import type { Transaction } from "~/shared/sync-client";

type Change = {
namespace: string;
patches: Array<Patch>;
};

type PatchData = {
transactions: Transaction<Change[]>[];
buildId: Build["id"];
Expand Down
Loading
Loading