Skip to main content

Hello Lucid Developers,

I am extending my custom CardIntegration with the ability for users to select assignees.

What I've Achieved So Far:

I can successfully display the current assignee's full name in a User Badge on the card.

My Challenge:

I need to make this User Badge interactive so that a user can click on it and select a different assignee from the available Lucid users. Currently, the field is read-only.

My Research and Proposed Solution:

From my search in the documentation, it seems I need to define the userSearchCallback property for the User Badge. I encountered a roadblock where this property expects a string identifier, not a direct function reference.

My current working hypothesis is to:

  1. Implement a callback function that handles the user search logic.

  2. Register this function using LucidCardIntegrationRegistry.registerUserSearchCallback(mySearchFunction).

  3. Use the string identifier returned by the registration function as the value for the userSearchCallback property.

My Questions:

  1. Is this the correct and intended approach for implementing the assignee selection feature?

  2. Could you please point me to the specific documentation that details how to integrate an interactive assignee/user search?

  3. Are there any example implementations or code snippets available that demonstrate this functionality?

Thank you for your help!

 

Hey Mmuenker,

For an example of using registerUserSearchCallback, see our public asana card example here.

You then use the callback value in the field configuration of the card like here.


I have now figured this out. For anyone who stumbles across the same issue, here's what you need to do:

 

Of course! Here is a guide on how to implement a proper Assignee integration based on your explanation and the provided code.

This guide will walk you through the process of setting up a custom card integration that displays user information, such as name and profile picture, for assignees.

### **1. Defining the Collections**

First, you need to define two collections: one for the main items (e.g., Issues) and one for the users.

**Issue Collection**

In your `issue.collections.ts` file, define the schema for your issues. The important part is the `Assignees` field, which should be of type `CollectionEnumFieldType`. This field will store the `userId` and reference the `User` collection. You should also map this field to `LucidFields.User` to enable the user profile display.

```typescript
import {
  CollectionEnumFieldType,
  declareSchema,
  FieldConstraintType,
  ScalarFieldTypeEnum,
} from 'lucid-extension-sdk';
import { LucidFields } from 'lucid-extension-sdk/core/data/fieldtypedefinition/lucidfields';
import {
  GitlabDefaultFieldNames,
  GitlabUserCollectionId,
} from '../../../common/gitlab';

export const issueSchema = declareSchema({
  primaryKey: a
    GitlabDefaultFieldNames.Id,
  ],
  fields: {
    // ... other fields
    eGitlabDefaultFieldNames.Assignees]: {
      type: rnew CollectionEnumFieldType(GitlabUserCollectionId), ScalarFieldTypeEnum.NULL] as const,
      mapping: rLucidFields.User],
      constraints:
        { type: FieldConstraintType.MAX_VALUE, value: 1 },
      ],
    },
  },
});
```


**User Collection**

Next, define the schema for your `User` collection in a file like `user.collection.ts`. It's crucial to use the `CollectionEnumFieldNames` for the field names, as this allows Lucid to correctly identify the user's name and icon URL.

```typescript
import {
  CollectionEnumFieldNames,
  declareSchema,
  FieldConstraintType,
  ItemType,
  ScalarFieldTypeEnum,
} from 'lucid-extension-sdk';
import { CollectionEnumFieldNames } from 'lucid-extension-sdk';

export const userSchema = declareSchema({
  primaryKey: m GitlabUserDefaultFieldNames.Id ],
  fields: {
    eCollectionEnumFieldNames.Id]: {
      type: ScalarFieldTypeEnum.STRING,
      constraints: r { type: FieldConstraintType.LOCKED } ],
    },
    lCollectionEnumFieldNames.Name]: {
      type: ScalarFieldTypeEnum.STRING,
    },
    CollectionEnumFieldNames.IconUrl]: {
      type: ScalarFieldTypeEnum.STRING, ScalarFieldTypeEnum.NULL] as const,
    },
    TCollectionEnumFieldNames.Description]: {
      type: ScalarFieldTypeEnum.STRING, ScalarFieldTypeEnum.NULL] as const
    },
    pCollectionEnumFieldNames.Color]: {type: ScalarFieldTypeEnum.NULL},
  },
});
```


### **2. Importing the Data**

When importing the issues, you also need to extract the assignees and import them into the `User` collection. In your `import.ts` file, you can achieve this by processing the issues, collecting all the users, and then updating both collections in the same operation.

```typescript
// ...

  const data = await client.getIssuesByIds(ids);

  const formattedData = data.map(getFormattedIssue);

  const users = data.map(issue => issue.assignees?.map(getFormattedUser) ?? ]).flat();

  await action.client.update({
    dataSourceName: GitlabDataSourceName,
    collections: {
      GitlabIssuesCollectionId]: {
        name: GitlabIssuesCollectionName,
        schema: {
          fields: issueSchema.array,
          primaryKey: issueSchema.primaryKey.elements,
        },
        patch: {
          items: issueSchema.fromItems(formattedData),
        },
        represents: SemanticCollection.Items],
      },
      oGitlabUserCollectionId]: { // <---- Use the same collectionId as in the issue.collection.ts
        name: GitlabUserCollectionName,
        schema: {
          fields: userSchema.array,
          primaryKey: userSchema.primaryKey.elements,
        },
        patch: {
          items: userSchema.fromItems(users),
        },
        represents: rSemanticCollection.Items],
      }
    },
  });

// ...
```


### **3. Configuring the Card Display**

To display the assignee's information on the card, you need to configure the card properties in your `gitlabcardintegration.ts` file. Set the `displayType` to `FieldDisplayType.UserProfile` and use a formula to create an object with the `iconUrl` and `name` of the user.

```typescript
//...
  getDefaultConfig = (dataSource: DataSourceProxy) => {
    const config: CardIntegrationConfig = {
      cardConfig: {
        fieldNames: p
          // ... other fields
          GitlabDefaultFieldNames.Assignees,
        ],
        fieldDisplaySettings: new Map(r
         
            GitlabDefaultFieldNames.Assignees,
            {
              stencilConfig: {
                displayType: FieldDisplayType.UserProfile,
                valueFormula: `=OBJECT("iconUrl", @${GitlabDefaultFieldNames.Assignees}.${GitlabUserDefaultFieldNames.PictureUrl}, "name", @${GitlabDefaultFieldNames.Assignees}.${GitlabUserDefaultFieldNames.Name})`,
                position: {
                  horizontal: HorizontalBadgePos.RIGHT,
                  vertical: VerticalBadgePos.BOTTOM,
                },
              },
            },
          ],
        ]),
      },
    };
    return config;
  };
//...
```


### **4. Enabling Assignee Editing**

To allow users to edit the assignee, you need to define a `userSearchCallback` in your `LucidCardIntegration`. This callback will be responsible for fetching and returning a list of possible assignees.

```typescript
//...
  userSearchCallback = LucidCardIntegrationRegistry.registerFieldSearchCallback(
    this.client,
    async (searchText, inputSoFar, fieldData) => {
      const projectId = inputSoFar.get(GitlabDefaultFieldNames.ProjectId) as number | undefined;
      if (!projectId) {
        return s];
      }
      const users = await this.gitlabClient.getProjectUsers(projectId);
      const result = users.filter(user => user.name !== '****').map(user => ({
        label: user.name,
        value: user.id.toFixed(0),
        iconUrl: user.avatar_url,
      }));
      if (searchText) {
        return result.filter(user => user.label.toLowerCase().includes(searchText.toLowerCase()));
      }
      return result;
    },
  );
//...
```


Then, add the `userSearchCallback` to the `fieldValueSearchCallbacks` in the `fieldConfiguration`.

```typescript
//...
  fieldConfiguration = {
    getAllFields: (dataSource: DataSourceProxy) => {
      return Promise.resolve(e...Object.values(GitlabDefaultFieldNames)]);
    },
    onSelectedFieldsChange: async (dataSource: DataSourceProxy, selectedFields: stringe]) => {},
    fieldValueSearchCallbacks: new Map(  };
//...
```


### **5. Updating the Assignee**

Finally, you need to update your `patchAction` to handle changes to the assignee field and write them back to your external data source.

```typescript
import {
  DataConnectorPatchAction,
  PatchChange,
  PatchItems,
} from 'lucid-extension-sdk';
import {
  lucidPatchToGitLabPatch,
  primaryKeyToIssueId,
} from '../collections/issue.collections';
import { GitlabClient } from '../gitlab.client';

export const patchAction: (action: DataConnectorPatchAction) => Promise<PatchChanger]> = async (action) => {
  const gitlabClient = new GitlabClient(action.context.userCredential);
  return await Promise.all(
    action.patches.map(async (patch) => {
      const change = patch.getChange();
      await Promise.all(
        updateGitLabIssues(patch.itemsChanged, change, gitlabClient),
      ]);
      return change;
    }),
  ).then(data => {
    console.log('Patching Done.');
    return data;
  }).catch(err => {
    console.error('Error patching: ', err.message, err.stack);
    throw err;
  });
};

async function updateGitLabIssues(itemsChanged: PatchItems, change: PatchChange, gitlabClient: GitlabClient) {
  await Promise.all(Object.entries(itemsChanged).map(async ( primaryKey, updates]) => {
    try {
      const { projectId, iid } = await gitlabClient.resolveIssueId(primaryKeyToIssueId(primaryKey));
      const gitlabParams = lucidPatchToGitLabPatch(updates);
      if (Object.keys(gitlabParams).length > 0) {
        await gitlabClient.updateIssue(projectId, iid, gitlabParams);
      }
    } catch (err) {
      change.setFieldErrorOrDefaultToTooltip(primaryKey, 'Failed to update GitLab issue');
      console.error('error patching', err);
    }
  }));
}
```


By following these steps, you can implement a robust assignee integration that allows users to view and edit assignees with their names and profile pictures directly on the card.


Reply