diff --git a/dashboard/src/pages/StudyConfPage/forms/destinations/Destination.test.tsx b/dashboard/src/pages/StudyConfPage/forms/destinations/Destination.test.tsx
index 487cfdb4..b49ecaf9 100644
--- a/dashboard/src/pages/StudyConfPage/forms/destinations/Destination.test.tsx
+++ b/dashboard/src/pages/StudyConfPage/forms/destinations/Destination.test.tsx
@@ -88,6 +88,54 @@ describe('a destination saved before ref_mode existed', () => {
});
});
+describe('a destination saved with an explicit mode', () => {
+ const stored: any = {
+ type: 'messenger',
+ name: 'fly messenger',
+ initial_shortcode: 'mnchweek',
+ welcome_message: 'Welcome!',
+ button_text: 'OK',
+ additional_metadata: null,
+ ref_mode: 'encoded',
+ };
+
+ it('preserves that mode through an unrelated edit', () => {
+ // The counterpart of the absent case: a conf that states a mode keeps it,
+ // because the forms spread `...data` rather than rebuilding the conf.
+ const update = jest.fn();
+
+ render(
+
+ );
+
+ fireEvent.change(screen.getByDisplayValue('Welcome!'), {
+ target: { name: 'welcome_message', value: 'Hello!' },
+ });
+
+ const [conf] = update.mock.calls[update.mock.calls.length - 1];
+ expect(conf.welcome_message).toBe('Hello!');
+ expect(conf.ref_mode).toBe('encoded');
+ });
+
+ it('does not warn while the mode still matches what was loaded', () => {
+ render(
+
+ );
+
+ expect(screen.queryByText(/rewrites every ad/i)).toBeNull();
+ });
+});
+
describe('a destination being added now', () => {
it('states its mode, because it is a new conf', () => {
const update = jest.fn();
diff --git a/dashboard/src/pages/StudyConfPage/forms/inferenceData/extraction.test.ts b/dashboard/src/pages/StudyConfPage/forms/inferenceData/extraction.test.ts
index dc64323c..542b132d 100644
--- a/dashboard/src/pages/StudyConfPage/forms/inferenceData/extraction.test.ts
+++ b/dashboard/src/pages/StudyConfPage/forms/inferenceData/extraction.test.ts
@@ -300,6 +300,25 @@ describe('extraction', () => {
it('gives a fly source with no variables one blank row', () => {
expect(generateLookupConfs('fly', [])).toHaveLength(1);
});
+
+ it('skips a variable the researcher has not named yet', () => {
+ // `name` is both the output name and the key into the ad's frozen row, so
+ // a lookup without one reads nothing off the row while presenting itself
+ // as configured.
+ const confs = generateLookupConfs('fly', ['gender', '', 'Age']);
+
+ expect(confs.map(c => c.name)).toEqual(['gender', 'Age']);
+ });
+
+ it('falls back to a blank row when no variable is named', () => {
+ // One unnamed variable is length 1, so a length check lets it through.
+ const confs = generateLookupConfs('fly', ['']);
+
+ expect(confs).toHaveLength(1);
+ expect(confs[0].name).toBe('');
+ expect(confs[0].mapping).toBe('raw');
+ expect(confs[0].location).toBe('');
+ });
});
describe('round trip into what swoosh reads', () => {
diff --git a/dashboard/src/pages/StudyConfPage/forms/inferenceData/generateLookupConfs.ts b/dashboard/src/pages/StudyConfPage/forms/inferenceData/generateLookupConfs.ts
index 59667391..e930d41b 100644
--- a/dashboard/src/pages/StudyConfPage/forms/inferenceData/generateLookupConfs.ts
+++ b/dashboard/src/pages/StudyConfPage/forms/inferenceData/generateLookupConfs.ts
@@ -57,9 +57,18 @@ export const generateLookupConfs = (
source: string,
variables: string[]
): Extraction[] => {
- if (source !== FLY_SOURCE || variables.length === 0) {
+ if (source !== FLY_SOURCE) {
return [blankConf()];
}
- return variables.map(lookupConf);
+ // An unnamed variable produces no conf. `name` is both the output name and
+ // the key into the ad's frozen row, so a lookup without one reads nothing off
+ // the row while presenting itself as configured — the researcher sees a
+ // filled-in row that can only ever yield nothing.
+ //
+ // Filtered rather than counted: one unnamed variable is length 1, so a
+ // `variables.length === 0` guard lets it straight through.
+ const named = variables.filter(name => !!name);
+
+ return named.length ? named.map(lookupConf) : [blankConf()];
};