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:
Robert
2023-07-18 16:31:46 +01:00
committed by GitHub
6 changed files with 86 additions and 47 deletions

View File

@@ -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));
}

View File

@@ -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}
/>

View File

@@ -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";
}

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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);
}
}