Skip to content

Add invite code pagination#830

Open
mohiuddinshahrukh wants to merge 20 commits into
devfrom
fix-46-invite-code-pagination
Open

Add invite code pagination#830
mohiuddinshahrukh wants to merge 20 commits into
devfrom
fix-46-invite-code-pagination

Conversation

@mohiuddinshahrukh

Copy link
Copy Markdown
Collaborator

Description

The Invite Codes table could hang or crash when a study has a large number of invite codes because the Designer loaded the full invite list at once.

Fix

This adds server-side pagination for invite codes in the Designer Recruit page. Invite codes are now fetched from Supabase in batches of 50 instead of loading the entire invite array into memory.

The table also shows page navigation controls with previous/next buttons and the current page number.

Acceptance Criteria

  • Invite codes are fetched in batches instead of loading the full list
  • Batch size is 50 invite codes per page
  • Previous / Next controls are available
  • Page number is shown
  • New invite codes appear immediately after saving

Testing

  • Ran flutter analyze in designer_v2
  • Tested the Recruit page locally
  • Created a new invite code and confirmed it appeared immediately without refreshing
  • Checked that pagination controls stayed on Page 1 for a small invite list

Example images, where the pagination size was kept to 10 only for showing how it looks.
image

image

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Visit the preview URL for this PR (updated for commit b53afb3):

(expires Tue, 21 Jul 2026 00:02:25 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

Sign: 2149dad49ed83535217e50d5c18c0c8c90da629b

@ibrahimozkn

Copy link
Copy Markdown
Contributor

Are we following the material guidelines on pagination widgets? Also for design consistency of the designer could you please check if we ever used fully rounded circular buttons? Or maybe rather than pagination we could switch to infinite scrolling? What do you think @johannesvedder

@mohiuddinshahrukh

Copy link
Copy Markdown
Collaborator Author

Thank you for for reviewing @ibrahimozkn

I'll adjust the UI accordingly.
Regarding the decision on infinte scroll or pagination: Depends what we are trying to achieve

Pagination:
For finding items quickly.
Seeing how many items might exist.
Not really oriented to capture attention and keep user engaged.
more functionality oriented.

Infinite scroll:
Easier on the eyes.
Not for quick finding and searching.
Not functionality-first oriented.
Oriented to caputre user attention and keep them engaged.

My two cents: Pagination is better here.

@johannesvedder

@ibrahimozkn

Copy link
Copy Markdown
Contributor

todo: preparing a documentation on design system w/ Material 3 guidelines (not related with #825)

@johannesvedder

johannesvedder commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

I think most important features that needs to be added are those that increase productivity. This includes, but not limited to:

@johannesvedder johannesvedder left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion(blocking): see comment above

@github-actions github-actions Bot added the core label Jul 13, 2026
@mohiuddinshahrukh

Copy link
Copy Markdown
Collaborator Author

Supabase migration needed for invite-code filters

Create migration file:

supabase/migrations/20260714110000_add_study_invite_filters_rpc.sql

BEGIN;

CREATE OR REPLACE FUNCTION public.get_study_invites_filtered(
  p_study_id uuid,
  p_query text DEFAULT NULL,
  p_enrolled_status text DEFAULT NULL,
  p_enrolled_min integer DEFAULT NULL,
  p_enrolled_max integer DEFAULT NULL,
  p_intervention_filter text DEFAULT NULL,
  p_created_from date DEFAULT NULL,
  p_created_to date DEFAULT NULL,
  p_updated_from date DEFAULT NULL,
  p_updated_to date DEFAULT NULL
)
RETURNS TABLE (
  code text,
  study_id uuid,
  preselected_intervention_ids text[],
  created_at timestamp with time zone,
  updated_at timestamp with time zone,
  study_invite_participant_count integer
)
LANGUAGE sql
STABLE
SET search_path = ''
AS $$
  WITH invites AS (
    SELECT
      si.code,
      si.study_id,
      si.preselected_intervention_ids,
      si.created_at,
      si.updated_at,
      public.study_invite_participant_count(si) AS study_invite_participant_count
    FROM public.study_invite AS si
    WHERE si.study_id = p_study_id
  )
  SELECT *
  FROM invites
  WHERE
    (p_query IS NULL OR invites.code ILIKE '%' || p_query || '%')
    AND (
      p_enrolled_status IS NULL
      OR (p_enrolled_status = 'unused' AND invites.study_invite_participant_count = 0)
      OR (p_enrolled_status = 'used' AND invites.study_invite_participant_count > 0)
    )
    AND (
      p_enrolled_min IS NULL
      OR invites.study_invite_participant_count >= p_enrolled_min
    )
    AND (
      p_enrolled_max IS NULL
      OR invites.study_invite_participant_count <= p_enrolled_max
    )
    AND (
      p_intervention_filter IS NULL
      OR (
        p_intervention_filter = 'default'
        AND COALESCE(array_length(invites.preselected_intervention_ids, 1), 0) = 0
      )
      OR (
        p_intervention_filter = 'intervention_a'
        AND COALESCE(array_length(invites.preselected_intervention_ids, 1), 0) >= 1
      )
      OR (
        p_intervention_filter = 'intervention_b'
        AND COALESCE(array_length(invites.preselected_intervention_ids, 1), 0) >= 2
      )
    )
    AND (p_created_from IS NULL OR invites.created_at::date >= p_created_from)
    AND (p_created_to IS NULL OR invites.created_at::date <= p_created_to)
    AND (p_updated_from IS NULL OR invites.updated_at::date >= p_updated_from)
    AND (p_updated_to IS NULL OR invites.updated_at::date <= p_updated_to);
$$;

CREATE OR REPLACE FUNCTION public.fetch_study_invites_filtered(
  p_study_id uuid,
  p_offset integer,
  p_limit integer,
  p_query text DEFAULT NULL,
  p_sort_by text DEFAULT 'created_at',
  p_ascending boolean DEFAULT false,
  p_enrolled_status text DEFAULT NULL,
  p_enrolled_min integer DEFAULT NULL,
  p_enrolled_max integer DEFAULT NULL,
  p_intervention_filter text DEFAULT NULL,
  p_created_from date DEFAULT NULL,
  p_created_to date DEFAULT NULL,
  p_updated_from date DEFAULT NULL,
  p_updated_to date DEFAULT NULL
)
RETURNS TABLE (
  code text,
  study_id uuid,
  preselected_intervention_ids text[],
  created_at timestamp with time zone,
  updated_at timestamp with time zone,
  study_invite_participant_count integer
)
LANGUAGE sql
STABLE
SET search_path = ''
AS $$
  SELECT *
  FROM public.get_study_invites_filtered(
    p_study_id,
    p_query,
    p_enrolled_status,
    p_enrolled_min,
    p_enrolled_max,
    p_intervention_filter,
    p_created_from,
    p_created_to,
    p_updated_from,
    p_updated_to
  )
  ORDER BY
    CASE WHEN p_sort_by = 'code' AND p_ascending THEN code END ASC,
    CASE WHEN p_sort_by = 'code' AND NOT p_ascending THEN code END DESC,
    CASE
      WHEN p_sort_by = 'enrolled' AND p_ascending
      THEN study_invite_participant_count
    END ASC,
    CASE
      WHEN p_sort_by = 'enrolled' AND NOT p_ascending
      THEN study_invite_participant_count
    END DESC,
    CASE WHEN p_sort_by = 'created_at' AND p_ascending THEN created_at END ASC,
    CASE WHEN p_sort_by = 'created_at' AND NOT p_ascending THEN created_at END DESC,
    CASE WHEN p_sort_by = 'updated_at' AND p_ascending THEN updated_at END ASC,
    CASE WHEN p_sort_by = 'updated_at' AND NOT p_ascending THEN updated_at END DESC,
    code ASC
  OFFSET p_offset
  LIMIT p_limit;
$$;

CREATE OR REPLACE FUNCTION public.count_study_invites_filtered(
  p_study_id uuid,
  p_query text DEFAULT NULL,
  p_enrolled_status text DEFAULT NULL,
  p_enrolled_min integer DEFAULT NULL,
  p_enrolled_max integer DEFAULT NULL,
  p_intervention_filter text DEFAULT NULL,
  p_created_from date DEFAULT NULL,
  p_created_to date DEFAULT NULL,
  p_updated_from date DEFAULT NULL,
  p_updated_to date DEFAULT NULL
)
RETURNS integer
LANGUAGE sql
STABLE
SET search_path = ''
AS $$
  SELECT count(1)::int
  FROM public.get_study_invites_filtered(
    p_study_id,
    p_query,
    p_enrolled_status,
    p_enrolled_min,
    p_enrolled_max,
    p_intervention_filter,
    p_created_from,
    p_created_to,
    p_updated_from,
    p_updated_to
  );
$$;

REVOKE EXECUTE ON FUNCTION public.get_study_invites_filtered(
  uuid, text, text, integer, integer, text, date, date, date, date
) FROM public, anon;
REVOKE EXECUTE ON FUNCTION public.fetch_study_invites_filtered(
  uuid, integer, integer, text, text, boolean, text, integer, integer, text, date, date, date, date
) FROM public, anon;
REVOKE EXECUTE ON FUNCTION public.count_study_invites_filtered(
  uuid, text, text, integer, integer, text, date, date, date, date
) FROM public, anon;

GRANT EXECUTE ON FUNCTION public.get_study_invites_filtered(
  uuid, text, text, integer, integer, text, date, date, date, date
) TO authenticated;
GRANT EXECUTE ON FUNCTION public.fetch_study_invites_filtered(
  uuid, integer, integer, text, text, boolean, text, integer, integer, text, date, date, date, date
) TO authenticated;
GRANT EXECUTE ON FUNCTION public.count_study_invites_filtered(
  uuid, text, text, integer, integer, text, date, date, date, date
) TO authenticated;

COMMIT;

Why needed

Designer invite-code filters now call server-side filtered page/count RPCs. Without this migration:

  • filter UI will open
  • API calls for filtered invite codes will fail
  • pagination/counts for filtered results will not work

@mohiuddinshahrukh

Copy link
Copy Markdown
Collaborator Author

Supabase migration also needed for invite-code enrolled count

Create migration file:

supabase/migrations/20260713142000_add_study_invite_participant_count.sql

BEGIN;

CREATE OR REPLACE FUNCTION public.study_invite_participant_count(
  invite public.study_invite
)
RETURNS integer
LANGUAGE sql
STABLE
SET search_path = ''
AS $$
  SELECT count(1)::int
  FROM public.study_subject
  WHERE study_subject.invite_code = invite.code
    AND study_subject.is_deleted = false;
$$;

REVOKE EXECUTE ON FUNCTION public.study_invite_participant_count(public.study_invite)
FROM public, anon;
GRANT EXECUTE ON FUNCTION public.study_invite_participant_count(public.study_invite)
TO authenticated;

COMMIT;

Why needed

Designer now shows and sorts by enrolled participant count for invite codes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants