Skip to content

Commit 1817281

Browse files
authored
Merge pull request #1073 from writer/AB-220
fix: prevent fallback value from overwriting cleared input by tracking dirty state - AB-220
2 parents 3ef2cd6 + 1170387 commit 1817281

File tree

3 files changed

+138
-11
lines changed

3 files changed

+138
-11
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { ref, nextTick } from "vue";
3+
import { buildMockComponent, buildMockCore } from "@/tests/mocks";
4+
import { FieldType } from "@/writerTypes";
5+
import { generateBuilderManager } from "./builderManager";
6+
import {
7+
useComponentFieldViewModel,
8+
Dependencies,
9+
} from "./useComponentFieldViewModel";
10+
11+
describe(useComponentFieldViewModel.name, () => {
12+
const componentId = "TestComponent";
13+
const fieldKey = "TestField";
14+
15+
let mockCore: ReturnType<typeof buildMockCore>;
16+
17+
function setupMockDependencies(
18+
componentId: string,
19+
fieldKey: string,
20+
): Dependencies {
21+
mockCore = buildMockCore();
22+
mockCore.core.addComponent(
23+
buildMockComponent({
24+
id: componentId,
25+
type: FieldType.Text,
26+
content: {
27+
[fieldKey]: undefined,
28+
},
29+
}),
30+
);
31+
32+
vi.spyOn(mockCore.core, "getComponentDefinition").mockReturnValue({
33+
name: "",
34+
description: "",
35+
fields: {
36+
[fieldKey]: {
37+
name: "",
38+
type: FieldType.Text,
39+
default: "default test",
40+
},
41+
},
42+
});
43+
44+
return {
45+
wf: mockCore.core,
46+
ssbm: generateBuilderManager(),
47+
};
48+
}
49+
50+
it("Default: uses field default from component definition when content is unset", async () => {
51+
const dependencies = setupMockDependencies(componentId, fieldKey);
52+
53+
const vm = useComponentFieldViewModel(
54+
{ componentId, fieldKey },
55+
dependencies,
56+
);
57+
58+
await nextTick();
59+
60+
expect(vm.value).toBe("default test");
61+
});
62+
63+
it("Default update: reactive defaultValue overrides definition and updates value when changed", async () => {
64+
const dependencies = setupMockDependencies(componentId, fieldKey);
65+
66+
const defaultValue = ref("ref default test");
67+
68+
const vm = useComponentFieldViewModel(
69+
{ componentId, fieldKey, defaultValue },
70+
dependencies,
71+
);
72+
73+
await nextTick();
74+
75+
expect(vm.value).toBe("ref default test");
76+
77+
defaultValue.value = "ref test";
78+
79+
await nextTick();
80+
81+
expect(vm.value).toBe("ref test");
82+
});
83+
84+
it("viewModel update: setting vm.value persists to component content and reflects in getter", async () => {
85+
const dependencies = setupMockDependencies(componentId, fieldKey);
86+
87+
const vm = useComponentFieldViewModel(
88+
{ componentId, fieldKey },
89+
dependencies,
90+
);
91+
92+
await nextTick();
93+
94+
expect(vm.value).toBe("default test");
95+
96+
vm.value = "new value";
97+
98+
await nextTick();
99+
100+
expect(
101+
mockCore.core.getComponentById(componentId).content[fieldKey],
102+
).toBe("new value");
103+
expect(vm.value).toBe("new value");
104+
105+
vm.value = "";
106+
107+
await nextTick();
108+
109+
expect(
110+
mockCore.core.getComponentById(componentId).content[fieldKey],
111+
).toBe("");
112+
expect(vm.value).toBe("");
113+
});
114+
});

src/ui/src/builder/useComponentFieldViewModel.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { computed, inject, MaybeRef, toValue, watch } from "vue";
1+
import { computed, inject, MaybeRef, ref, toValue, watch } from "vue";
22
import { BuilderManager, Core } from "@/writerTypes";
33
import injectionKeys from "@/injectionKeys";
44
import { useLogger } from "@/composables/useLogger";
55
import { useComponentActions } from "./useComponentActions";
66

7-
type Params = {
7+
export type Params = {
88
componentId: MaybeRef<string>;
99
fieldKey: MaybeRef<string>;
1010
defaultValue?: MaybeRef<string | undefined>;
1111
};
1212

13-
type Dependencies = {
13+
export type Dependencies = {
1414
wf?: Core;
1515
ssbm?: BuilderManager;
1616
};
@@ -72,16 +72,30 @@ export function useComponentFieldViewModel(
7272
setContentValue(component.value.id, toValue(params.fieldKey), value);
7373
}
7474

75+
const isDirty = ref(false);
76+
77+
watch(
78+
[() => toValue(params.componentId), () => toValue(params.fieldKey)],
79+
() => {
80+
isDirty.value = false;
81+
},
82+
);
83+
7584
const fieldValue = computed<string>(() => {
76-
return (
77-
component.value?.content?.[toValue(params.fieldKey)] ||
78-
fallbackValue.value
79-
);
85+
const val = component.value?.content?.[toValue(params.fieldKey)];
86+
87+
if (isDirty.value && typeof val === "string") {
88+
return val;
89+
}
90+
91+
return val || fallbackValue.value;
8092
});
8193

8294
const fieldViewModel = computed<string>({
8395
get: () => fieldValue.value,
8496
set: (value: string) => {
97+
isDirty.value = true;
98+
8599
setFieldValue(value);
86100
},
87101
});

src/ui/src/composables/useLogger.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
/* eslint-disable no-console */
22

3+
export type ILogger = Pick<typeof console, "log" | "warn" | "info" | "error">;
4+
35
/**
46
* A simple abstraction to use logger in the application. For the moment, it's just a proxy to `console`, but it can be plugged to any library later.
57
*/
6-
export function useLogger(): Pick<
7-
typeof console,
8-
"log" | "warn" | "info" | "error"
9-
> {
8+
export function useLogger(): ILogger {
109
return {
1110
log: console.log,
1211
warn: console.warn,

0 commit comments

Comments
 (0)