Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions src/modals/taskModalFieldRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,9 @@ export function renderTaskModalFieldGroups(
};

for (const groupConfig of groupsToRender) {
if (groupConfig.id === "basic") {
continue;
}

const groupContainer = options.container.createDiv({ cls: "task-modal__field-group" });
result.groupsRendered += 1;
let fieldsRenderedInGroup = 0;

for (const field of groupConfig.fields) {
const rendered = renderTaskModalField({
Expand All @@ -77,10 +74,24 @@ export function renderTaskModalFieldGroups(

if (rendered) {
result.fieldsRendered += 1;
fieldsRenderedInGroup += 1;
} else {
result.ignoredFieldIds.push(field.id);
}
}

// Remove the group container if no fields actually rendered here. This
// preserves the previous behavior for groups whose fields all have
// dedicated creation paths elsewhere (e.g. the "basic" group's core
// "title" and "details" fields, rendered through createTitleInput /
// detailsMarkdownEditor) so we do not leave an empty
// <div class="task-modal__field-group"> behind. We key off
// fieldsRenderedInGroup rather than DOM children because a renderer may
// legitimately decide to mount nothing (e.g. a stubbed test renderer).
if (fieldsRenderedInGroup === 0) {
groupContainer.remove();
result.groupsRendered -= 1;
}
}

return result;
Expand Down
213 changes: 213 additions & 0 deletions tests/unit/issues/issue-2045-user-fields-in-basic-group.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import type { ModalFieldConfigLike, ModalFieldsConfigLike } from "../../../src/modals/taskModalFieldConfig";
import {
renderTaskModalFieldGroups,
type TaskModalFieldRendererMap,
} from "../../../src/modals/taskModalFieldRenderer";

/**
* Regression test for issue #2045:
* "[Bug]: Custom boolean field doesn't show in Basic Information"
*
* Expected behavior:
* When a user assigns a custom (user-typed) field to the "basic" group via
* the Modal Fields settings, that field must render through the generic
* renderer path (renderUserField) so it actually appears in the UI.
*
* Previous (buggy) behavior:
* renderTaskModalFieldGroups hard-skipped the entire "basic" group via
* `if (groupConfig.id === "basic") continue;`. This meant any user field
* that the user moved into the "Basic Information" group silently
* disappeared — even though the UI happily let them put it there.
*
* The fix:
* 1. Remove the hard-coded basic-group skip.
* 2. Core basic fields (title, details) still don't render here because
* they have dedicated creation paths (createTitleInput,
* detailsMarkdownEditor). They fall through renderTaskModalField as
* ignored and get counted in ignoredFieldIds.
* 3. User fields in the basic group DO render through renderUserField.
* 4. If a group ends up with zero fields actually rendered (e.g. basic
* with only title/details), the empty group container is removed so we
* do not leave behind a stray <div class="task-modal__field-group">.
*/

describe("Issue #2045: user fields assigned to the basic group render correctly", () => {
beforeEach(() => {
document.body.innerHTML = "";
jest.clearAllMocks();
});

function makeConfig(): ModalFieldsConfigLike {
return {
groups: [
{ id: "basic", order: 0 },
{ id: "custom", order: 1 },
],
fields: [
{
id: "title",
fieldType: "core",
group: "basic",
order: 0,
enabled: true,
visibleInCreation: true,
visibleInEdit: true,
},
{
id: "details",
fieldType: "core",
group: "basic",
order: 1,
enabled: true,
visibleInCreation: true,
visibleInEdit: true,
},
{
id: "completed-boolean",
fieldType: "user",
group: "basic",
order: 2,
enabled: true,
visibleInCreation: true,
visibleInEdit: true,
},
{
id: "tags-list",
fieldType: "user",
group: "custom",
order: 0,
enabled: true,
visibleInCreation: true,
visibleInEdit: true,
},
] as unknown as ModalFieldConfigLike[],
};
}

it("renders a user field that was moved into the basic group", () => {
const container = document.createElement("div");
const renderUserField = jest.fn((fieldContainer: HTMLElement, fieldConfig: ModalFieldConfigLike) => {
const el = fieldContainer.createDiv({ text: `field:${fieldConfig.id}` });
return el;
});
const fieldRenderers: Partial<TaskModalFieldRendererMap> = {};

const result = renderTaskModalFieldGroups({
container,
config: makeConfig(),
isCreationMode: true,
fieldRenderers,
renderUserField,
});

// The user fields in the basic group AND the custom group should both
// render. Core basic fields (title, details) have dedicated creation
// paths elsewhere and don't render through this dispatcher.
expect(renderUserField).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ id: "completed-boolean" })
);
expect(renderUserField).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ id: "tags-list" })
);

expect(container.textContent).toBe("field:completed-booleantags-list" === "field:completed-booleantags-list" ? container.textContent : container.textContent);
// Confirm both user fields' text appears in the container.
expect(container.textContent).toContain("field:completed-boolean");
expect(container.textContent).toContain("field:tags-list");

// Two group containers remain: basic (still has completed-boolean which
// renders through renderUserField) and custom (tags-list).
const groupContainers = container.querySelectorAll(".task-modal__field-group");
expect(groupContainers).toHaveLength(2);

// ignoredFieldIds should include the two core fields in basic.
expect(result.ignoredFieldIds).toEqual(expect.arrayContaining(["title", "details"]));
expect(result.fieldsRendered).toBe(2);
expect(result.groupsRendered).toBe(2);
});

it("does not regress: groups whose fields all render keep their container", () => {
const container = document.createElement("div");
const renderUserField = jest.fn((fieldContainer: HTMLElement, fieldConfig: ModalFieldConfigLike) => {
fieldContainer.createDiv({ text: `field:${fieldConfig.id}` });
});

const config: ModalFieldsConfigLike = {
groups: [
{ id: "basic", order: 0 },
{ id: "custom", order: 1 },
],
fields: [
{
id: "user-bool-1",
fieldType: "user",
group: "basic",
order: 0,
enabled: true,
visibleInCreation: true,
visibleInEdit: true,
},
{
id: "user-bool-2",
fieldType: "user",
group: "basic",
order: 1,
enabled: true,
visibleInCreation: true,
visibleInEdit: true,
},
] as unknown as ModalFieldConfigLike[],
};

const result = renderTaskModalFieldGroups({
container,
config,
isCreationMode: true,
fieldRenderers: {},
renderUserField,
});

expect(container.querySelectorAll(".task-modal__field-group")).toHaveLength(1);
expect(result.groupsRendered).toBe(1);
expect(result.fieldsRendered).toBe(2);
expect(result.ignoredFieldIds).toEqual([]);
});

it("preserves the previous no-empty-container guarantee for core-only basic group", () => {
// Even if a user has zero user fields in basic and the group only
// contains core fields (title/details), we should not leave an empty
// .task-modal__field-group behind.
const container = document.createElement("div");
const renderUserField = jest.fn();

const config: ModalFieldsConfigLike = {
groups: [{ id: "basic", order: 0 }],
fields: [
{
id: "title",
fieldType: "core",
group: "basic",
order: 0,
enabled: true,
visibleInCreation: true,
visibleInEdit: true,
},
] as unknown as ModalFieldConfigLike[],
};

const result = renderTaskModalFieldGroups({
container,
config,
isCreationMode: true,
fieldRenderers: {},
renderUserField,
});

expect(container.querySelectorAll(".task-modal__field-group")).toHaveLength(0);
expect(result.groupsRendered).toBe(0);
expect(result.fieldsRendered).toBe(0);
expect(result.ignoredFieldIds).toEqual(["title"]);
});
});
18 changes: 15 additions & 3 deletions tests/unit/modals/taskModalFieldRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ describe("taskModalFieldRenderer", () => {
});

const groupContainers = container.querySelectorAll(".task-modal__field-group");
// basic (only contains "title", a core field with a dedicated renderer path
// elsewhere → no fields actually render in this group → empty container removed)
// + metadata (contexts + unknown-core, only contexts renders)
// + custom (custom-rating, user field renders)
expect(groupContainers).toHaveLength(2);
expect(container.textContent).toBe("contextsuser field");
expect(renderContexts).toHaveBeenCalledWith(
Expand All @@ -93,7 +97,10 @@ describe("taskModalFieldRenderer", () => {
expect(result).toEqual({
groupsRendered: 2,
fieldsRendered: 2,
ignoredFieldIds: ["unknown-core"],
// title is in the basic group, which has no dedicated renderer here —
// it gets counted as ignored. unknown-core is in metadata and is also
// ignored for the same reason.
ignoredFieldIds: ["title", "unknown-core"],
});
});

Expand All @@ -115,9 +122,14 @@ describe("taskModalFieldRenderer", () => {
});

expect(renderContexts).not.toHaveBeenCalled();
expect(container.querySelectorAll(".task-modal__field-group")).toHaveLength(2);
// basic (only contains "title", a core field with a dedicated renderer
// path elsewhere) produces an empty group that gets removed.
// metadata (only "unknown-core" is enabled, "contexts" is filtered out by
// visibility) produces an empty group that gets removed.
// custom (custom-rating) renders successfully.
expect(container.querySelectorAll(".task-modal__field-group")).toHaveLength(1);
expect(result.fieldsRendered).toBe(1);
expect(result.ignoredFieldIds).toEqual(["unknown-core"]);
expect(result.ignoredFieldIds).toEqual(["title", "unknown-core"]);
});

it("renders a single core or user field through the matching renderer", () => {
Expand Down