Skip to content

Commit a9d31e7

Browse files
authored
feat: allow editing remote resources (#5274)
Did you ever wonder why you can't edit inherited data variables? We wondered the same. Now you can edit variables and resources from anywhere deep in the tree. https://github.com/user-attachments/assets/9e117fc7-9329-4b38-8a2f-c97360bc3e7d
1 parent df9fecf commit a9d31e7

File tree

4 files changed

+217
-159
lines changed

4 files changed

+217
-159
lines changed

apps/builder/app/builder/features/settings-panel/controls/resource-control.tsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,25 @@ import {
2525
BindingPopover,
2626
type BindingVariant,
2727
} from "~/builder/shared/binding-popover";
28-
import { $props, $resources } from "~/shared/nano-states";
28+
import {
29+
$dataSources,
30+
$props,
31+
$resources,
32+
$variableValuesByInstanceSelector,
33+
} from "~/shared/nano-states";
2934
import { computeExpression } from "~/shared/data-variables";
3035
import { updateWebstudioData } from "~/shared/instance-utils";
31-
import { $selectedInstance } from "~/shared/awareness";
3236
import {
33-
$selectedInstanceResourceScope,
37+
$selectedInstance,
38+
$selectedInstanceKeyWithRoot,
39+
$selectedPage,
40+
} from "~/shared/awareness";
41+
import {
3442
UrlField,
3543
MethodField,
3644
Headers,
3745
parseResource,
46+
getResourceScopeForInstance,
3847
} from "../resource-panel";
3948
import { type ControlProps, useLocalValue, VerticalLayout } from "../shared";
4049
import { PropertyLabel } from "../property-label";
@@ -77,6 +86,23 @@ const ResourceButton = forwardRef<
7786
});
7887
ResourceButton.displayName = "ResourceButton";
7988

89+
const $selectedInstanceResourceScope = computed(
90+
[
91+
$selectedPage,
92+
$selectedInstanceKeyWithRoot,
93+
$variableValuesByInstanceSelector,
94+
$dataSources,
95+
],
96+
(page, instanceKey, variableValuesByInstanceSelector, dataSources) => {
97+
return getResourceScopeForInstance({
98+
page,
99+
instanceKey,
100+
dataSources,
101+
variableValuesByInstanceSelector,
102+
});
103+
}
104+
);
105+
80106
const ResourceForm = ({ resource }: { resource: Resource }) => {
81107
const { scope, aliases } = useStore($selectedInstanceResourceScope);
82108
const [url, setUrl] = useState(resource.url);

apps/builder/app/builder/features/settings-panel/resource-panel.tsx

Lines changed: 136 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
useState,
1212
} from "react";
1313
import { useStore } from "@nanostores/react";
14-
import { Resource, type DataSource } from "@webstudio-is/sdk";
14+
import {
15+
DataSources,
16+
Resource,
17+
type DataSource,
18+
type Page,
19+
} from "@webstudio-is/sdk";
1520
import {
1621
encodeDataVariableId,
1722
generateObjectExpression,
@@ -53,14 +58,16 @@ import {
5358
EditorDialogButton,
5459
EditorDialogControl,
5560
} from "~/builder/shared/code-editor-base";
56-
import { parseCurl, type CurlRequest } from "./curl";
5761
import {
5862
$selectedInstance,
59-
$selectedInstanceKeyWithRoot,
63+
$selectedInstancePathWithRoot,
6064
$selectedPage,
65+
getInstanceKey,
66+
type InstancePath,
6167
} from "~/shared/awareness";
6268
import { updateWebstudioData } from "~/shared/instance-utils";
6369
import { rebindTreeVariablesMutable } from "~/shared/data-variables";
70+
import { parseCurl, type CurlRequest } from "./curl";
6471

6572
export const parseResource = ({
6673
id,
@@ -408,85 +415,114 @@ export const Headers = ({
408415
);
409416
};
410417

411-
const $hiddenDataSourceIds = computed(
412-
[$dataSources, $selectedPage],
413-
(dataSources, page) => {
414-
const dataSourceIds = new Set<DataSource["id"]>();
415-
for (const dataSource of dataSources.values()) {
416-
// hide collection item and component parameters from resources
417-
// to prevent waterfall and loop requests ans not complicate compiler
418-
if (dataSource.type === "parameter") {
419-
dataSourceIds.add(dataSource.id);
420-
}
421-
// prevent resources using data of other resources
422-
if (dataSource.type === "resource") {
423-
dataSourceIds.add(dataSource.id);
424-
}
418+
export const getResourceScopeForInstance = ({
419+
page,
420+
instanceKey,
421+
dataSources,
422+
variableValuesByInstanceSelector,
423+
}: {
424+
page: undefined | Page;
425+
instanceKey: undefined | string;
426+
dataSources: DataSources;
427+
variableValuesByInstanceSelector: Map<string, Map<string, unknown>>;
428+
}) => {
429+
const scope: Record<string, unknown> = {};
430+
const aliases = new Map<string, string>();
431+
const variableValues = new Map<DataSource["id"], unknown>();
432+
const hiddenDataSourceIds = new Set<DataSource["id"]>();
433+
for (const dataSource of dataSources.values()) {
434+
// hide collection item and component parameters from resources
435+
// to prevent waterfall and loop requests ans not complicate compiler
436+
if (dataSource.type === "parameter") {
437+
hiddenDataSourceIds.add(dataSource.id);
425438
}
426-
if (page?.systemDataSourceId) {
427-
dataSourceIds.delete(page.systemDataSourceId);
439+
// prevent resources using data of other resources
440+
if (dataSource.type === "resource") {
441+
hiddenDataSourceIds.add(dataSource.id);
428442
}
429-
return dataSourceIds;
430443
}
431-
);
432-
433-
export const $selectedInstanceResourceScope = computed(
434-
[
435-
$selectedInstanceKeyWithRoot,
436-
$variableValuesByInstanceSelector,
437-
$dataSources,
438-
$hiddenDataSourceIds,
439-
],
440-
(
441-
instanceKey,
442-
variableValuesByInstanceSelector,
443-
dataSources,
444-
hiddenDataSourceIds
445-
) => {
446-
const scope: Record<string, unknown> = {};
447-
const aliases = new Map<string, string>();
448-
const variableValues = new Map<DataSource["id"], unknown>();
449-
if (instanceKey === undefined) {
450-
return { variableValues, scope, aliases };
451-
}
452-
const values = variableValuesByInstanceSelector.get(instanceKey);
453-
if (values) {
454-
for (const [dataSourceId, value] of values) {
455-
if (hiddenDataSourceIds.has(dataSourceId)) {
456-
continue;
457-
}
458-
let dataSource = dataSources.get(dataSourceId);
459-
if (dataSourceId === SYSTEM_VARIABLE_ID) {
460-
dataSource = systemParameter;
461-
}
462-
if (dataSource) {
463-
const name = encodeDataVariableId(dataSourceId);
464-
variableValues.set(dataSourceId, value);
465-
scope[name] = value;
466-
aliases.set(name, dataSource.name);
467-
}
444+
if (page?.systemDataSourceId) {
445+
hiddenDataSourceIds.delete(page.systemDataSourceId);
446+
}
447+
const values = variableValuesByInstanceSelector.get(instanceKey ?? "");
448+
if (values) {
449+
for (const [dataSourceId, value] of values) {
450+
if (hiddenDataSourceIds.has(dataSourceId)) {
451+
continue;
452+
}
453+
let dataSource = dataSources.get(dataSourceId);
454+
if (dataSourceId === SYSTEM_VARIABLE_ID) {
455+
dataSource = systemParameter;
456+
}
457+
if (dataSource) {
458+
const name = encodeDataVariableId(dataSourceId);
459+
variableValues.set(dataSourceId, value);
460+
scope[name] = value;
461+
aliases.set(name, dataSource.name);
468462
}
469463
}
470-
return { variableValues, scope, aliases };
471464
}
472-
);
465+
return { variableValues, scope, aliases };
466+
};
467+
468+
const getVariableInstanceKey = ({
469+
variable,
470+
instancePath,
471+
}: {
472+
variable: undefined | DataSource;
473+
instancePath: undefined | InstancePath;
474+
}) => {
475+
if (instancePath === undefined) {
476+
return;
477+
}
478+
// find instance key for variable instance
479+
for (const { instance, instanceSelector } of instancePath) {
480+
if (instance.id === variable?.scopeInstanceId) {
481+
return getInstanceKey(instanceSelector);
482+
}
483+
}
484+
// and fallback to currently selected instance
485+
return getInstanceKey(instancePath[0].instanceSelector);
486+
};
473487

474488
const useScope = ({ variable }: { variable?: DataSource }) => {
475-
const { scope: scopeWithCurrentVariable, aliases } = useStore(
476-
$selectedInstanceResourceScope
489+
return useStore(
490+
useMemo(
491+
() =>
492+
computed(
493+
[
494+
$selectedPage,
495+
$selectedInstancePathWithRoot,
496+
$variableValuesByInstanceSelector,
497+
$dataSources,
498+
],
499+
(
500+
page,
501+
instancePath,
502+
variableValuesByInstanceSelector,
503+
dataSources
504+
) => {
505+
const { scope, aliases } = getResourceScopeForInstance({
506+
page,
507+
instanceKey: getVariableInstanceKey({
508+
variable,
509+
instancePath,
510+
}),
511+
dataSources,
512+
variableValuesByInstanceSelector,
513+
});
514+
// prevent showing currently edited variable in suggestions
515+
// to avoid cirular dependeny
516+
const newScope = { ...scope };
517+
if (variable) {
518+
delete newScope[encodeDataVariableId(variable.id)];
519+
}
520+
return { scope: newScope, aliases };
521+
}
522+
),
523+
[variable]
524+
)
477525
);
478-
const currentVariableId = variable?.id;
479-
// prevent showing currently edited variable in suggestions
480-
// to avoid cirular dependeny
481-
const scope = useMemo(() => {
482-
if (currentVariableId === undefined) {
483-
return scopeWithCurrentVariable;
484-
}
485-
const newScope: Record<string, unknown> = { ...scopeWithCurrentVariable };
486-
delete newScope[encodeDataVariableId(currentVariableId)];
487-
return newScope;
488-
}, [scopeWithCurrentVariable, currentVariableId]);
489-
return { scope, aliases };
490526
};
491527

492528
type PanelApi = {
@@ -635,8 +671,10 @@ export const ResourceForm = forwardRef<
635671

636672
useImperativeHandle(ref, () => ({
637673
save: (formData) => {
638-
const selectedInstance = $selectedInstance.get();
639-
if (selectedInstance === undefined) {
674+
// preserve existing instance scope when edit
675+
const scopeInstanceId =
676+
variable?.scopeInstanceId ?? $selectedInstance.get()?.id;
677+
if (scopeInstanceId === undefined) {
640678
return;
641679
}
642680
const name = z.string().parse(formData.get("name"));
@@ -647,17 +685,18 @@ export const ResourceForm = forwardRef<
647685
});
648686
const newVariable: DataSource = {
649687
id: variable?.id ?? nanoid(),
650-
// preserve existing instance scope when edit
651-
scopeInstanceId: variable?.scopeInstanceId ?? selectedInstance.id,
688+
scopeInstanceId,
652689
name,
653690
type: "resource",
654691
resourceId: newResource.id,
655692
};
656693
updateWebstudioData((data) => {
657694
data.dataSources.set(newVariable.id, newVariable);
658695
data.resources.set(newResource.id, newResource);
659-
const startingInstanceId = selectedInstance.id;
660-
rebindTreeVariablesMutable({ startingInstanceId, ...data });
696+
rebindTreeVariablesMutable({
697+
startingInstanceId: scopeInstanceId,
698+
...data,
699+
});
661700
});
662701
},
663702
}));
@@ -756,8 +795,10 @@ export const SystemResourceForm = forwardRef<
756795

757796
useImperativeHandle(ref, () => ({
758797
save: (formData) => {
759-
const selectedInstance = $selectedInstance.get();
760-
if (selectedInstance === undefined) {
798+
// preserve existing instance scope when edit
799+
const scopeInstanceId =
800+
variable?.scopeInstanceId ?? $selectedInstance.get()?.id;
801+
if (scopeInstanceId === undefined) {
761802
return;
762803
}
763804
const name = z.string().parse(formData.get("name"));
@@ -771,17 +812,18 @@ export const SystemResourceForm = forwardRef<
771812
};
772813
const newVariable: DataSource = {
773814
id: variable?.id ?? nanoid(),
774-
// preserve existing instance scope when edit
775-
scopeInstanceId: variable?.scopeInstanceId ?? selectedInstance.id,
815+
scopeInstanceId,
776816
name,
777817
type: "resource",
778818
resourceId: newResource.id,
779819
};
780820
updateWebstudioData((data) => {
781821
data.dataSources.set(newVariable.id, newVariable);
782822
data.resources.set(newResource.id, newResource);
783-
const startingInstanceId = selectedInstance.id;
784-
rebindTreeVariablesMutable({ startingInstanceId, ...data });
823+
rebindTreeVariablesMutable({
824+
startingInstanceId: scopeInstanceId,
825+
...data,
826+
});
785827
});
786828
},
787829
}));
@@ -865,8 +907,10 @@ export const GraphqlResourceForm = forwardRef<
865907

866908
useImperativeHandle(ref, () => ({
867909
save: (formData) => {
868-
const selectedInstance = $selectedInstance.get();
869-
if (selectedInstance === undefined) {
910+
// preserve existing instance scope when edit
911+
const scopeInstanceId =
912+
variable?.scopeInstanceId ?? $selectedInstance.get()?.id;
913+
if (scopeInstanceId === undefined) {
870914
return;
871915
}
872916
const name = z.string().parse(formData.get("name"));
@@ -887,17 +931,18 @@ export const GraphqlResourceForm = forwardRef<
887931
};
888932
const newVariable: DataSource = {
889933
id: variable?.id ?? nanoid(),
890-
// preserve existing instance scope when edit
891-
scopeInstanceId: variable?.scopeInstanceId ?? selectedInstance.id,
934+
scopeInstanceId,
892935
name,
893936
type: "resource",
894937
resourceId: newResource.id,
895938
};
896939
updateWebstudioData((data) => {
897940
data.dataSources.set(newVariable.id, newVariable);
898941
data.resources.set(newResource.id, newResource);
899-
const startingInstanceId = selectedInstance.id;
900-
rebindTreeVariablesMutable({ startingInstanceId, ...data });
942+
rebindTreeVariablesMutable({
943+
startingInstanceId: scopeInstanceId,
944+
...data,
945+
});
901946
});
902947
},
903948
}));

0 commit comments

Comments
 (0)