Merge pull request #2611 from github/robertbrignull/data-unsaved-checkbox
Show whether changes to a method are saved or not
This commit is contained in:
@@ -17,7 +17,6 @@ import { DataExtensionEditorViewState } from "../../data-extensions-editor/share
|
||||
import { ModeledMethodsList } from "./ModeledMethodsList";
|
||||
import { percentFormatter } from "./formatters";
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
import { groupMethods } from "../../data-extensions-editor/shared/sorting";
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
text-align: center;
|
||||
@@ -75,7 +74,9 @@ export function DataExtensionsEditor({
|
||||
const [externalApiUsages, setExternalApiUsages] = useState<
|
||||
ExternalApiUsage[]
|
||||
>(initialExternalApiUsages);
|
||||
const [unsavedModels, setUnsavedModels] = useState<Set<string>>(new Set());
|
||||
const [modifiedSignatures, setModifiedSignatures] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const [modeledMethods, setModeledMethods] = useState<
|
||||
Record<string, ModeledMethod>
|
||||
@@ -119,15 +120,11 @@ export function DataExtensionsEditor({
|
||||
),
|
||||
};
|
||||
});
|
||||
setUnsavedModels(
|
||||
(oldUnsavedModels) =>
|
||||
setModifiedSignatures(
|
||||
(oldModifiedSignatures) =>
|
||||
new Set([
|
||||
...oldUnsavedModels,
|
||||
...modelsAffectedByNewModeledMethods(
|
||||
msg.modeledMethods,
|
||||
externalApiUsages,
|
||||
viewState?.mode ?? Mode.Application,
|
||||
),
|
||||
...oldModifiedSignatures,
|
||||
...Object.keys(msg.modeledMethods),
|
||||
]),
|
||||
);
|
||||
break;
|
||||
@@ -145,7 +142,7 @@ export function DataExtensionsEditor({
|
||||
return () => {
|
||||
window.removeEventListener("message", listener);
|
||||
};
|
||||
}, [externalApiUsages, viewState?.mode]);
|
||||
}, []);
|
||||
|
||||
const modeledPercentage = useMemo(
|
||||
() => calculateModeledPercentage(externalApiUsages),
|
||||
@@ -160,8 +157,9 @@ export function DataExtensionsEditor({
|
||||
...oldModeledMethods,
|
||||
[method.signature]: model,
|
||||
}));
|
||||
setUnsavedModels(
|
||||
(oldUnsavedModels) => new Set([...oldUnsavedModels, modelName]),
|
||||
setModifiedSignatures(
|
||||
(oldModifiedSignatures) =>
|
||||
new Set([...oldModifiedSignatures, method.signature]),
|
||||
);
|
||||
},
|
||||
[],
|
||||
@@ -179,12 +177,11 @@ export function DataExtensionsEditor({
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
});
|
||||
setUnsavedModels(new Set());
|
||||
setModifiedSignatures(new Set());
|
||||
}, [externalApiUsages, modeledMethods]);
|
||||
|
||||
const onSaveModelClick = useCallback(
|
||||
(
|
||||
modelName: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
) => {
|
||||
@@ -193,10 +190,12 @@ export function DataExtensionsEditor({
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
});
|
||||
setUnsavedModels((oldUnsavedModels) => {
|
||||
const newUnsavedModels = new Set(oldUnsavedModels);
|
||||
newUnsavedModels.delete(modelName);
|
||||
return newUnsavedModels;
|
||||
setModifiedSignatures((oldModifiedSignatures) => {
|
||||
const newModifiedSignatures = new Set([...oldModifiedSignatures]);
|
||||
for (const externalApiUsage of externalApiUsages) {
|
||||
newModifiedSignatures.delete(externalApiUsage.signature);
|
||||
}
|
||||
return newModifiedSignatures;
|
||||
});
|
||||
},
|
||||
[],
|
||||
@@ -317,8 +316,8 @@ export function DataExtensionsEditor({
|
||||
</ButtonsContainer>
|
||||
<ModeledMethodsList
|
||||
externalApiUsages={externalApiUsages}
|
||||
unsavedModels={unsavedModels}
|
||||
modeledMethods={modeledMethods}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
viewState={viewState}
|
||||
onChange={onChange}
|
||||
onSaveModelClick={onSaveModelClick}
|
||||
@@ -331,15 +330,3 @@ export function DataExtensionsEditor({
|
||||
</DataExtensionsEditorContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function modelsAffectedByNewModeledMethods(
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
mode: Mode,
|
||||
): string[] {
|
||||
const signatures = new Set(Object.keys(modeledMethods));
|
||||
const affectedExternalApiUsages = externalApiUsages.filter(
|
||||
(externalApiUsage) => signatures.has(externalApiUsage.signature),
|
||||
);
|
||||
return Object.keys(groupMethods(affectedExternalApiUsages, mode));
|
||||
}
|
||||
|
||||
@@ -71,15 +71,14 @@ type Props = {
|
||||
libraryVersion?: string;
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
modifiedSignatures: Set<string>;
|
||||
viewState: DataExtensionEditorViewState;
|
||||
hasUnsavedChanges: boolean;
|
||||
onChange: (
|
||||
modelName: string,
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
modeledMethod: ModeledMethod,
|
||||
) => void;
|
||||
onSaveModelClick: (
|
||||
modelName: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
) => void;
|
||||
@@ -95,8 +94,8 @@ export const LibraryRow = ({
|
||||
libraryVersion,
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
modifiedSignatures,
|
||||
viewState,
|
||||
hasUnsavedChanges,
|
||||
onChange,
|
||||
onSaveModelClick,
|
||||
onGenerateFromLlmClick,
|
||||
@@ -137,11 +136,11 @@ export const LibraryRow = ({
|
||||
|
||||
const handleSave = useCallback(
|
||||
async (e: React.MouseEvent) => {
|
||||
onSaveModelClick(title, externalApiUsages, modeledMethods);
|
||||
onSaveModelClick(externalApiUsages, modeledMethods);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
[title, externalApiUsages, modeledMethods, onSaveModelClick],
|
||||
[externalApiUsages, modeledMethods, onSaveModelClick],
|
||||
);
|
||||
|
||||
const onChangeWithModelName = useCallback(
|
||||
@@ -151,6 +150,12 @@ export const LibraryRow = ({
|
||||
[onChange, title],
|
||||
);
|
||||
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
return externalApiUsages.some((externalApiUsage) =>
|
||||
modifiedSignatures.has(externalApiUsage.signature),
|
||||
);
|
||||
}, [externalApiUsages, modifiedSignatures]);
|
||||
|
||||
return (
|
||||
<LibraryContainer>
|
||||
<TitleContainer onClick={toggleExpanded} aria-expanded={isExpanded}>
|
||||
@@ -195,6 +200,7 @@ export const LibraryRow = ({
|
||||
<ModeledMethodDataGrid
|
||||
externalApiUsages={externalApiUsages}
|
||||
modeledMethods={modeledMethods}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
mode={viewState.mode}
|
||||
onChange={onChangeWithModelName}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
VSCodeCheckbox,
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
VSCodeLink,
|
||||
@@ -20,6 +19,10 @@ import { extensiblePredicateDefinitions } from "../../data-extensions-editor/pre
|
||||
import { Mode } from "../../data-extensions-editor/shared/mode";
|
||||
import { Dropdown } from "../common/Dropdown";
|
||||
import { MethodClassifications } from "./MethodClassifications";
|
||||
import {
|
||||
ModelingStatus,
|
||||
ModelingStatusIndicator,
|
||||
} from "./ModelingStatusIndicator";
|
||||
|
||||
const ApiOrMethodCell = styled(VSCodeDataGridCell)`
|
||||
display: flex;
|
||||
@@ -51,6 +54,7 @@ const modelTypeOptions: Array<{ value: ModeledMethodType; label: string }> = [
|
||||
type Props = {
|
||||
externalApiUsage: ExternalApiUsage;
|
||||
modeledMethod: ModeledMethod | undefined;
|
||||
methodIsUnsaved: boolean;
|
||||
mode: Mode;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
@@ -59,11 +63,12 @@ type Props = {
|
||||
};
|
||||
|
||||
export const MethodRow = (props: Props) => {
|
||||
const { externalApiUsage, modeledMethod } = props;
|
||||
const { externalApiUsage, modeledMethod, methodIsUnsaved } = props;
|
||||
|
||||
const methodCanBeModeled =
|
||||
!externalApiUsage.supported ||
|
||||
(modeledMethod && modeledMethod?.type !== "none");
|
||||
(modeledMethod && modeledMethod?.type !== "none") ||
|
||||
methodIsUnsaved;
|
||||
|
||||
if (methodCanBeModeled) {
|
||||
return <ModelableMethodRow {...props} />;
|
||||
@@ -73,7 +78,8 @@ export const MethodRow = (props: Props) => {
|
||||
};
|
||||
|
||||
function ModelableMethodRow(props: Props) {
|
||||
const { externalApiUsage, modeledMethod, mode, onChange } = props;
|
||||
const { externalApiUsage, modeledMethod, methodIsUnsaved, mode, onChange } =
|
||||
props;
|
||||
|
||||
const argumentsList = useMemo(() => {
|
||||
if (externalApiUsage.methodParameters === "()") {
|
||||
@@ -192,10 +198,12 @@ function ModelableMethodRow(props: Props) {
|
||||
: undefined;
|
||||
const showKindCell = predicate?.supportedKinds;
|
||||
|
||||
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
|
||||
|
||||
return (
|
||||
<VSCodeDataGridRow>
|
||||
<ApiOrMethodCell gridColumn={1}>
|
||||
<VSCodeCheckbox />
|
||||
<ModelingStatusIndicator status={modelingStatus} />
|
||||
<ExternalApiUsageName {...props} />
|
||||
{mode === Mode.Application && (
|
||||
<UsagesButton onClick={jumpToUsage}>
|
||||
@@ -251,7 +259,7 @@ function UnmodelableMethodRow(props: Props) {
|
||||
return (
|
||||
<VSCodeDataGridRow>
|
||||
<ApiOrMethodCell gridColumn={1}>
|
||||
<VSCodeCheckbox />
|
||||
<ModelingStatusIndicator status="saved" />
|
||||
<ExternalApiUsageName {...props} />
|
||||
{mode === Mode.Application && (
|
||||
<UsagesButton onClick={jumpToUsage}>
|
||||
@@ -287,3 +295,17 @@ function sendJumpToUsageMessage(externalApiUsage: ExternalApiUsage) {
|
||||
location: externalApiUsage.usages[0].url,
|
||||
});
|
||||
}
|
||||
|
||||
function getModelingStatus(
|
||||
modeledMethod: ModeledMethod | undefined,
|
||||
methodIsUnsaved: boolean,
|
||||
): ModelingStatus {
|
||||
if (modeledMethod) {
|
||||
if (methodIsUnsaved) {
|
||||
return "unsaved";
|
||||
} else if (modeledMethod.type !== "none") {
|
||||
return "saved";
|
||||
}
|
||||
}
|
||||
return "unmodeled";
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { sortMethods } from "../../data-extensions-editor/shared/sorting";
|
||||
type Props = {
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
modifiedSignatures: Set<string>;
|
||||
mode: Mode;
|
||||
onChange: (
|
||||
externalApiUsage: ExternalApiUsage,
|
||||
@@ -24,6 +25,7 @@ type Props = {
|
||||
export const ModeledMethodDataGrid = ({
|
||||
externalApiUsages,
|
||||
modeledMethods,
|
||||
modifiedSignatures,
|
||||
mode,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
@@ -56,6 +58,7 @@ export const ModeledMethodDataGrid = ({
|
||||
key={externalApiUsage.signature}
|
||||
externalApiUsage={externalApiUsage}
|
||||
modeledMethod={modeledMethods[externalApiUsage.signature]}
|
||||
methodIsUnsaved={modifiedSignatures.has(externalApiUsage.signature)}
|
||||
mode={mode}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
@@ -12,8 +12,8 @@ import { DataExtensionEditorViewState } from "../../data-extensions-editor/share
|
||||
|
||||
type Props = {
|
||||
externalApiUsages: ExternalApiUsage[];
|
||||
unsavedModels: Set<string>;
|
||||
modeledMethods: Record<string, ModeledMethod>;
|
||||
modifiedSignatures: Set<string>;
|
||||
viewState: DataExtensionEditorViewState;
|
||||
onChange: (
|
||||
modelName: string,
|
||||
@@ -21,7 +21,6 @@ type Props = {
|
||||
modeledMethod: ModeledMethod,
|
||||
) => void;
|
||||
onSaveModelClick: (
|
||||
modelName: string,
|
||||
externalApiUsages: ExternalApiUsage[],
|
||||
modeledMethods: Record<string, ModeledMethod>,
|
||||
) => void;
|
||||
@@ -38,8 +37,8 @@ const libraryNameOverrides: Record<string, string> = {
|
||||
|
||||
export const ModeledMethodsList = ({
|
||||
externalApiUsages,
|
||||
unsavedModels,
|
||||
modeledMethods,
|
||||
modifiedSignatures,
|
||||
viewState,
|
||||
onChange,
|
||||
onSaveModelClick,
|
||||
@@ -79,8 +78,8 @@ export const ModeledMethodsList = ({
|
||||
title={libraryNameOverrides[libraryName] ?? libraryName}
|
||||
libraryVersion={libraryVersions[libraryName]}
|
||||
externalApiUsages={grouped[libraryName]}
|
||||
hasUnsavedChanges={unsavedModels.has(libraryName)}
|
||||
modeledMethods={modeledMethods}
|
||||
modifiedSignatures={modifiedSignatures}
|
||||
viewState={viewState}
|
||||
onChange={onChange}
|
||||
onSaveModelClick={onSaveModelClick}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { assertNever } from "../../common/helpers-pure";
|
||||
import { Codicon } from "../common/icon/Codicon";
|
||||
|
||||
export type ModelingStatus = "unmodeled" | "unsaved" | "saved";
|
||||
|
||||
interface Props {
|
||||
status: ModelingStatus;
|
||||
}
|
||||
|
||||
export function ModelingStatusIndicator({ status }: Props) {
|
||||
switch (status) {
|
||||
case "unmodeled":
|
||||
return <Codicon name="circle-large-outline" label="Method not modeled" />;
|
||||
case "unsaved":
|
||||
return <Codicon name="pass" label="Changes have not been saved" />;
|
||||
case "saved":
|
||||
return <Codicon name="pass-filled" label="Method modeled" />;
|
||||
default:
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user