Skip to content

Conversation

Anemy
Copy link
Member

@Anemy Anemy commented Sep 9, 2025

COMPASS-9742

Adds some field editing e2e testing as well. Left two comments/questions for reviewers.

Screenshot 2025-09-11 at 9 28 40 AM
add.field.to.object.mp4

@github-actions github-actions bot added the feat label Sep 9, 2025
@Anemy Anemy added the feature flagged PRs labeled with this label will not be included in the release notes of the next release label Sep 9, 2025
export const getBaseFieldsFromSchema = ({
jsonSchema,
}: {
jsonSchema: MongoDBJSONSchema;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love two functions that do pretty much the same thing, but it seemed better than doing something like:

export const getFieldsFromSchema = ({
  jsonSchema,
  renderOptions,
}: {
  jsonSchema: MongoDBJSONSchema;
  renderOptions?: {
    highlightedFields: FieldPath[];
    selectedField?: FieldPath;
    onClickAddNestedField: (parentFieldPath: string[]) => void;
  };
}): NodeProps['fields'] => {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big disclaimer that I'm just sharing some thoughts on this. I think what you have here is probably good enough (at least for now for sure), but as it bothers you I thought I'd try to provide some guidance here:

You are right that having two separate functions like that is not great: there's now no easy way to make sure that what we get for calculation purposes is the same as what we get for rendering purposes. That's not the only solution for this problem though 🙂 I mentioned this before, it's all about how you compose this: you have "default" level here, just pure data that contains serializeable information that should be enough to render this, and then you have "rendering" level that takes this data (maybe more stuff that's only rendering related, like those callbacks) and maps this to UI. The latter doesn't even need to live outside of React code FWIW, but if needed the logic of this mapping can be encapsulated into a function too.

So to summarise something that would allow you to compose the code in a way that builds up on the "base" functions and adds UI on top of it would probably be the cleanest way to deal with this instead of keeping the "base" completely separate. You can even have more "layers" here, why not, it doesn't really all need to happen in just two steps. Something like this pseudo-code:

type Node = NodeProps /* & { any extra stuff we need as long as it doesn't conflict with NodeProps } */
type Field = NodeProps['fields'][number]

function getBaseNode(collection: Collection): Node;

function getBaseFields(collection: Collection): Field[];

function getBaseNodes(collections: Collection[]): Node[] {
  return collections.map(collection => {
    return {
      ...getBaseNode(collection),
      fields: getBaseFields(collection)
    }
  });
}

function getUINode(node: Node): Node

function getUIFields(fields: Field[]): Field[]

function getNodesForUI(nodes: Node[]) {
  return nodes.map(node => {
    return {
      ...getUINode(node),
      fields: getUIFields(fields)
    }
  })
}

This has a slight downside of iterating over fields multiple times, but I would be hesitant to consider this an issue unless we really see this performing badly and even if it does, now you have shared building blocks to build two separate functions, but share the logic, we also have memoization tools to help us deal with that.

@Anemy Anemy marked this pull request as ready for review September 11, 2025 13:29
@Anemy Anemy requested a review from a team as a code owner September 11, 2025 13:29
@Anemy Anemy requested review from nbbeeken and Copilot September 11, 2025 13:29
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds functionality to allow users to add fields to object types directly from the diagram view in the data modeling feature. The implementation includes both the UI components to display "add field" buttons on object fields and the corresponding E2E tests to validate the new functionality.

Key changes:

  • Added "add field" button to object field types in the diagram
  • Created new E2E test helper functions for diagram interaction
  • Extended field editing functionality to support nested field creation

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/compass-e2e-tests/tests/data-modeling-tab.test.ts Added comprehensive E2E test for field editing and new drag helper function
packages/compass-e2e-tests/helpers/selectors.ts Added selectors for new add field buttons and field type combobox
packages/compass-e2e-tests/helpers/commands/set-multi-combo-box-value.ts New helper command for setting multiple combobox values
packages/compass-data-modeling/src/utils/schema.ts Enhanced field name generation to support nested object paths
packages/compass-data-modeling/src/utils/nodes-and-edges.tsx Added object field type component and nested field functionality
packages/compass-data-modeling/src/components/diagram/object-field-type.tsx New component rendering object type with add field button
packages/compass-data-modeling/src/store/diagram.ts Added action handler for creating nested fields
packages/compass-data-modeling/src/components/diagram-editor.tsx Connected nested field creation to the diagram editor

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

export const getBaseFieldsFromSchema = ({
jsonSchema,
}: {
jsonSchema: MongoDBJSONSchema;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big disclaimer that I'm just sharing some thoughts on this. I think what you have here is probably good enough (at least for now for sure), but as it bothers you I thought I'd try to provide some guidance here:

You are right that having two separate functions like that is not great: there's now no easy way to make sure that what we get for calculation purposes is the same as what we get for rendering purposes. That's not the only solution for this problem though 🙂 I mentioned this before, it's all about how you compose this: you have "default" level here, just pure data that contains serializeable information that should be enough to render this, and then you have "rendering" level that takes this data (maybe more stuff that's only rendering related, like those callbacks) and maps this to UI. The latter doesn't even need to live outside of React code FWIW, but if needed the logic of this mapping can be encapsulated into a function too.

So to summarise something that would allow you to compose the code in a way that builds up on the "base" functions and adds UI on top of it would probably be the cleanest way to deal with this instead of keeping the "base" completely separate. You can even have more "layers" here, why not, it doesn't really all need to happen in just two steps. Something like this pseudo-code:

type Node = NodeProps /* & { any extra stuff we need as long as it doesn't conflict with NodeProps } */
type Field = NodeProps['fields'][number]

function getBaseNode(collection: Collection): Node;

function getBaseFields(collection: Collection): Field[];

function getBaseNodes(collections: Collection[]): Node[] {
  return collections.map(collection => {
    return {
      ...getBaseNode(collection),
      fields: getBaseFields(collection)
    }
  });
}

function getUINode(node: Node): Node

function getUIFields(fields: Field[]): Field[]

function getNodesForUI(nodes: Node[]) {
  return nodes.map(node => {
    return {
      ...getUINode(node),
      fields: getUIFields(fields)
    }
  })
}

This has a slight downside of iterating over fields multiple times, but I would be hesitant to consider this an issue unless we really see this performing badly and even if it does, now you have shared building blocks to build two separate functions, but share the logic, we also have memoization tools to help us deal with that.

@@ -183,6 +186,8 @@ const DiagramContent: React.FunctionComponent<{
: undefined,
onClickAddNewFieldToCollection: () =>
onAddNewFieldToCollection(coll.ns),
onClickAddNestedField: (parentFieldPath: string[]) =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was testing that the next field name logic is working fine and managed to get into some weird state where if I rename the field and then without unfocusing first I click on the "add new field" button, the new field is created with the name "interact" in the diagram, but the drawer shows the correct one, I can't figure out where the "interact" part is even coming from

Kapture 2025-09-12 at 12 26 07

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh, I was rewatching the gif and it seems to replace the next field in the document that is a sibling of an object this is being added for

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooo nice catch

Copy link
Collaborator

@nbbeeken nbbeeken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to get my compass review legs under me, read through and looks, good Sergey caught some more interesting things than I ^

curious about one small thing

Comment on lines +7 to +9
const fieldPathToTraverse = [...parentFieldPath];
let parentJSONSchema: MongoDBJSONSchema | undefined = jsonSchema;
while (fieldPathToTraverse.length > 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NBD but it stands out to me that we clone the array and shift out, but don't actually rely on the modification of the array later? could this just be for-of over the input array?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat feature flagged PRs labeled with this label will not be included in the release notes of the next release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants