Can not filter only jira-admins and jira-users with Search Users field

Can not filter only jira-admins and jira-user with Search Users field

Original Source Code:

export async function searchAssignees({ q, limit = 50 } = {}) {
  if (!q || q.length < 1) {
    return { assignees: [] };
  }

  try {
    const safeLimit = Number(limit) && Number(limit) > 0 ? Math.min(Number(limit), 100) : 50;
    const url = route`/rest/api/3/users/search?startAt=0&maxResults=${safeLimit}`;
    
    const res = await api.asUser().requestJira(url, {
      method: 'GET',
      headers: { Accept: 'application/json' }
    });

    if (res.status >= 400) {
      const text = await res.text();
      console.error('searchAssignees API error', res.status, text);
      throw new Error(`Jira API error ${res.status}`);
    }

    const json = await res.json();
    
    // Filter users by search term and limit results to max 50
    const assignees = (Array.isArray(json) ? json : json.users || [])
      .filter(u => {
        const displayName = (u.displayName || '').toLowerCase();
        const searchTerm = (q || '').toLowerCase();
        return displayName.includes(searchTerm);
      })
      .slice(0, safeLimit)
      .map(u => ({
        accountId: u.accountId,
        displayName: u.displayName || u.name || 'Unknown',
        avatarUrl: u.avatarUrls?.["24x24"] || u.avatarUrls?.["16x16"] || ''
      }));

    return { assignees };
  } catch (err) {
    console.error('searchAssignees error:', err);
    throw err;
  }
}

Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-user-search/#api-rest-api-3-user-search-get

Fixed Source Code by Copilot not work:

export async function searchAssignees({ q, limit = 50 } = {}) {
  if (!q || q.length < 1) {
    return { assignees: [] };
  }

  try {
    const safeLimit = Number(limit) && Number(limit) > 0 ? Math.min(Number(limit), 50) : 50;

    // Construct JQL for user search
    // We strictly filter by membership in "jira-users" or "jira-administrators"
    let jql = 'groups in ("jira-users", "jira-administrators")';

    if (q && q.trim().length > 0) {
      // Escape content for JQL string "..."
      const term = q.trim().replace(/"/g, '\\"');
      // Append search condition
      jql += ` AND (displayName ~ "${term}*" OR emailAddress ~ "${term}*")`;
    }

    const url = route`/rest/api/3/user/search?query=${jql}&startAt=0&maxResults=${safeLimit}`;

    const res = await api.asUser().requestJira(url, {
      method: 'GET',
      headers: { Accept: 'application/json' }
    });

    if (res.status >= 400) {
      const text = await res.text();
      console.error('searchAssignees API error', res.status, text);
      throw new Error(`Jira API error ${res.status}`);
    }

    const json = await res.json();

    // Defensively handle the response: ensure json exists and is an object
    if (!json || typeof json !== 'object') {
      console.error('searchAssignees: invalid response structure', json);
      return { assignees: [] };
    }

    // /rest/api/3/user/search/query returns { values: [...users], ... }
    // Ensure json.values is an array before mapping
    const userList = Array.isArray(json.values) ? json.values : (Array.isArray(json) ? json : []);
    
    const assignees = userList.map(u => ({
      accountId: u.accountId,
      displayName: u.displayName || u.name || 'Unknown',
      avatarUrl: u.avatarUrls?.["24x24"] || u.avatarUrls?.["16x16"] || ''
    }));

    return { assignees };
  } catch (err) {
    console.error('searchAssignees error:', err);
    throw err;
  }
}

Working code made by Copilot but not filter JQL with displayName:

export async function searchAssignees({ q, limit = 50 } = {}) {
  if (!q || q.length < 1) {
    return { assignees: [] };
  }

  try {
    const safeLimit = Number(limit) && Number(limit) > 0 ? Math.min(Number(limit), 50) : 50;

    // Use the /rest/api/3/users/search endpoint
    // The query parameter is a simple string matched against displayName and emailAddress
    const searchTerm = String(q).trim();
    const encodedQuery = encodeURIComponent(searchTerm);
    const url = route`/rest/api/3/users/search?query=${encodedQuery}&maxResults=${safeLimit}`;

    console.info('searchAssignees: calling', url, 'with query', searchTerm);

    const res = await api.asUser().requestJira(url, {
      method: 'GET',
      headers: { Accept: 'application/json' }
    });

    if (res.status >= 400) {
      const text = await res.text();
      console.error('searchAssignees API error', res.status, text);
      throw new Error(`Jira API error ${res.status}`);
    }

    const json = await res.json();
    console.info('searchAssignees response count:', Array.isArray(json) ? json.length : 0);

    // Response is an array of users - defensively ensure we have an array before mapping
    if (!Array.isArray(json)) {
      console.error('searchAssignees: expected array response, got:', typeof json, json);
      return { assignees: [] };
    }

    // Filter to only include active atlassian users (not app users or inactive users)
    const assignees = json
      .filter(u => u.accountType === 'atlassian' && u.active !== false)
      .map(u => ({
        accountId: u.accountId,
        displayName: u.displayName || u.name || 'Unknown',
        avatarUrl: u.avatarUrls?.["24x24"] || u.avatarUrls?.["16x16"] || ''
      }));

    console.info('searchAssignees: returning', assignees.length, 'active atlassian users');
    return { assignees };
  } catch (err) {
    console.error('searchAssignees error:', err);
    throw err;
  }
}

An another not list by displayName:

export async function searchAssignees({ q, limit = 50 } = {}) {
  if (!q || q.length < 1) {
    return { assignees: [] };
  }

  try {
    const safeLimit = Number(limit) && Number(limit) > 0 ? Math.min(Number(limit), 50) : 50;

    // Use simple search string - the API matches against displayName and emailAddress
    const searchTerm = String(q).trim();

    console.info('searchAssignees: searching for:', searchTerm);

    // Use route template tag as required by Forge
    const url = route`/rest/api/3/users/search?query=${searchTerm}&maxResults=${safeLimit}`;

    const res = await api.asUser().requestJira(url, {
      method: 'GET',
      headers: { Accept: 'application/json' }
    });

    if (res.status >= 400) {
      const text = await res.text();
      console.error('searchAssignees API error', res.status, text);
      throw new Error(`Jira API error ${res.status}`);
    }

    const json = await res.json();
    console.info('searchAssignees response count:', Array.isArray(json) ? json.length : 0);

    // Response is an array of users - defensively ensure we have an array before mapping
    if (!Array.isArray(json)) {
      console.error('searchAssignees: expected array response, got:', typeof json, json);
      return { assignees: [] };
    }

    // Filter to only include active atlassian users (not app users or inactive users)
    const assignees = json
      .filter(u => u.accountType === 'atlassian' && u.active !== false)
      .map(u => ({
        accountId: u.accountId,
        displayName: u.displayName || u.name || 'Unknown',
        avatarUrl: u.avatarUrls?.["24x24"] || u.avatarUrls?.["16x16"] || ''
      }));

    console.info('searchAssignees: returning', assignees.length, 'active atlassian users');
    return { assignees };
  } catch (err) {
    console.error('searchAssignees error:', err);
    throw err;
  }
}

This not work either:

export async function searchAssignees({ q, limit = 50 } = {}) {
  if (!q || q.length < 1) {
    return { assignees: [] };
  }

  try {
    const safeLimit = Number(limit) && Number(limit) > 0 ? Math.min(Number(limit), 50) : 50;

    // Construct JQL equivalent: filter by group membership and display name
    // Note: /rest/api/3/users/search doesn't accept JQL in query parameter,
    // so we build it for reference and use simple search + client-side filtering
    const searchTerm = String(q).trim();
    let jql = 'groups in ("jira-users", "jira-administrators")';
    if (searchTerm) {
      const escapedTerm = searchTerm.replace(/"/g, '\\"');
      jql += ` AND displayName ~ "${escapedTerm}*"`;
    }
    console.info('searchAssignees: JQL equivalent:', jql);

    // The /users/search API only accepts simple text search, not JQL
    // Search by displayName/emailAddress prefix
    const url = route`/rest/api/3/users/search?query=${searchTerm}&maxResults=${safeLimit}`;

    const res = await api.asUser().requestJira(url, {
      method: 'GET',
      headers: { Accept: 'application/json' }
    });

    if (res.status >= 400) {
      const text = await res.text();
      console.error('searchAssignees API error', res.status, text);
      throw new Error(`Jira API error ${res.status}`);
    }

    const json = await res.json();
    console.info('searchAssignees raw response count:', Array.isArray(json) ? json.length : 0);

    // Response is an array of users - defensively ensure we have an array before mapping
    if (!Array.isArray(json)) {
      console.error('searchAssignees: expected array response, got:', typeof json, json);
      return { assignees: [] };
    }

    // Client-side filtering: only active atlassian users in jira-users or jira-administrators
    // (This simulates the JQL filter since the API doesn't support JQL in query parameter)
    const assignees = json
      .filter(u => {
        // Must be atlassian user (not app/bot)
        if (u.accountType !== 'atlassian') return false;
        // Must be active
        if (u.active === false) return false;
        return true;
      })
      .map(u => ({
        accountId: u.accountId,
        displayName: u.displayName || u.name || 'Unknown',
        avatarUrl: u.avatarUrls?.["24x24"] || u.avatarUrls?.["16x16"] || ''
      }));

    console.info('searchAssignees: returning', assignees.length, 'filtered users');
    return { assignees };
  } catch (err) {
    console.error('searchAssignees error:', err);
    throw err;
  }
}

This code filter users but not with JQL, so can anyone tell how use JQL?

export async function searchAssignees({ q, limit = 50 } = {}) {
  if (!q || q.length < 1) {
    return { assignees: [] };
  }

  try {
    const safeLimit = Number(limit) && Number(limit) > 0 ? Math.min(Number(limit), 50) : 50;

    // Construct JQL equivalent: filter by group membership and display name
    // Note: /rest/api/3/users/search doesn't accept JQL in query parameter,
    // so we build it for reference and use simple search + client-side filtering
    const searchTerm = String(q).trim();
    let jql = 'groups in ("jira-users", "jira-administrators")';
    if (searchTerm) {
      const escapedTerm = searchTerm.replace(/"/g, '\\"');
      jql += ` AND displayName ~ "${escapedTerm}*"`;
    }

    // The /users/search API only accepts simple text search, not JQL
    // Search by displayName/emailAddress prefix
    const url = route`/rest/api/3/users/search?query=${searchTerm}&maxResults=${safeLimit}`;

    const res = await api.asUser().requestJira(url, {
      method: 'GET',
      headers: { Accept: 'application/json' }
    });

    if (res.status >= 400) {
      const text = await res.text();
      console.error('searchAssignees API error', res.status, text);
      throw new Error(`Jira API error ${res.status}`);
    }

    const json = await res.json();

    // Response is an array of users - defensively ensure we have an array before mapping
    if (!Array.isArray(json)) {
      console.error('searchAssignees: expected array response, got:', typeof json, json);
      return { assignees: [] };
    }

    // Client-side filtering: only active atlassian users in jira-users or jira-administrators
    // (This simulates the JQL filter since the API doesn't support JQL in query parameter)
    const assignees = json
      .filter(u => {
        // Must be atlassian user (not app/bot)
        if (u.accountType !== 'atlassian') return false;
        // Must be active
        if (u.active === false) return false;
        // Must match searchTerm in displayName (case-insensitive prefix match)
        if (searchTerm && !(u.displayName || '').toLowerCase().includes(searchTerm.toLowerCase())) {
          return false;
        }
        return true;
      })
      .map(u => ({
        accountId: u.accountId,
        displayName: u.displayName || u.name || 'Unknown',
        avatarUrl: u.avatarUrls?.["24x24"] || u.avatarUrls?.["16x16"] || ''
      }));

    return { assignees };
  } catch (err) {
    console.error('searchAssignees error:', err);
    throw err;
  }
}