Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with values in compareFunction for nested objects #181

Open
rokamon1997 opened this issue Feb 4, 2025 · 7 comments
Open

Issue with values in compareFunction for nested objects #181

rokamon1997 opened this issue Feb 4, 2025 · 7 comments
Assignees
Labels
question A request for information, clarification, or support

Comments

@rokamon1997
Copy link

Hello, I don't know If I am approaching this correctly but I have an issue when I want to write a custom compareFunction for a column.

So I understand that some columns can't be sorted correctly if the cell data is a custom object. Then I write a custom compareFunction with custom sort logic and it works fine.

But now I have the following (a bit simplified for the sake of simplicity) object structure for objects in my table:

{
  createDate: Date;
  followers: {
    userId: string;
    email: string;
  }[];
  task: {
    title: string;
    dueDate: Date;
    priority: EnumPriority;
    state: EnumState;
    effort: number[];
    owner: {
      userId: string;
      email: string;
    };
  };
}

So an incident that has additional information stored in a nested task property.

And I can't get the task property values in my compareFunction.

If I write a custom compareFunction for the createDate it works, but if I want to write a custom compareFunction for the tasks dueDate or priority for example I always get the values as undefined.
The only way I get this to work is to name my column property task and than use that as a deeper property of the task and write a compareFunction for the it; but I can only do this once, since the name of the column property can't be repeated.

The valuePrepareFunction works great but it has nothing to do with the compareFunction because the values passed into the compare functions are always the raw values of the object property with the same name. IDK, but maybe we need something like access to both rows data inside the compareFunction to than be able to compare the properties of a nested object.

Here is a simple example of the columns definition where the problem is visible:

const columns = {
  createDate: {
    sortDirection: 'desc',
    title: translateService.instant('INCIDENT.CREATE-DATE'),
    type: 'text',
    valuePrepareFunction: (value: Date) => {
      return formatDateLocalized(value);
    },
    compareFunction: sortByDate, // this works because createDate is a direct property of the object
    width: '50%',
  },
  title: {
    title: translateService.instant('COMMON.TITLE'),
    type: 'text',
    valuePrepareFunction: (value: unknown, cell: Cell) => {
      const rowData: IncidentFragment = cell.getRow().getData();
      return rowData.task?.title;
    },
    compareFunction: sortStringsFunction, // this DOESN'T work and values are undefined because this is a property of the nested task object
    width: '50%',
  },
}

Unless there is a different approach that I don't know of.

Thanks.

@rokamon1997
Copy link
Author

Maybe adding a property like compareBaseProperty could be added. With it you would know what to pass into the compareFuction if the column property name isn't matched.

For example compareBaseProperty: 'task' or even compareBaseProperty: 'task.title'.

@uap-universe
Copy link
Collaborator

There had been attempts to support access to nested properties in the past, but in general it's a bad idea and almost never working reliably.

Instead, you can simply add getter functions to your class which provide the necessary information top-level and use the names of the getters for the columns.

For example

  // add this to your data class
  get task_title(): string {
      return this.task.title;
  }

  // and in the Settings object for the smart table,
  // just use the derived property without special compare function
  task_title: {
    title: translateService.instant('COMMON.TITLE'),
    type: 'text',
    width: 50%,
  }

@uap-universe uap-universe self-assigned this Feb 4, 2025
@uap-universe uap-universe added the question A request for information, clarification, or support label Feb 4, 2025
@rokamon1997
Copy link
Author

rokamon1997 commented Feb 4, 2025

Great, thanks! I haven't considered this before.

I have to make copies of the data since it is not extensible by default but I think this will work since I don't have large numbers of data.

For anyone in the future that might need this (this adds the getter for each property in the task property):

export function addTaskGetters(objectsArray: IncidentFragment[]) {
  return objectsArray.map((obj: IncidentFragment) => {
    const objCopy = { ...obj };

    if (objCopy.task && typeof objCopy.task === 'object') {
      for (const key of Object.keys(objCopy.task)) {
        const getterName = `task${key.charAt(0).toUpperCase()}${key.slice(1)}`;

        Object.defineProperty(objCopy, getterName, {
          get: function () {
            if (this.task) {
              return this.task[key];
            }

            return undefined;
          },
          enumerable: true,
          configurable: true,
        });
      }
    }

    return objCopy;
  });
}

Note: I prefix the property names with the 'task' keyword.

@uap-universe
Copy link
Collaborator

uap-universe commented Feb 4, 2025

Wouldn't it be easier to create a second View Class, extending the Data class and defining the getters as usual and then assign the original data?

Something like this:

export class IncidentFragmentTableRow extends IncidentFragment {
  get task_title(): string {
      return this.task.title;
  }
}

export function convertToRowData(objectsArray: IncidentFragment[]) {
  return objectsArray.map((obj: IncidentFragment) => {

    const rowData = new IncidentFragmentTableRow();
    Objects.assign(rowData, obj); // alternatively deepCopy if you need to

    return rowData;
  });
}

@rokamon1997
Copy link
Author

rokamon1997 commented Feb 5, 2025

Yes this makes more sense. I will use this approach. Thanks!

EDIT:
I just noticed that i can't extend IncidentFragment since its a type not a class.

@uap-universe
Copy link
Collaborator

Okay, well.. then you can define a class with the same layout as your type alias plus the additional getter. Typescript actually doesn't care for inheritance and that the stuff properly typechecks. As long as your class declares the same properties, you can use Objects.assign() and you are fine.

@rokamon1997
Copy link
Author

Thanks I'll think about it. Seems like more work to provide same layout and than additional getters. Especially since I won't be using type-safe props of the class but just to match the columns definition names with property names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question A request for information, clarification, or support
Projects
None yet
Development

No branches or pull requests

2 participants