use-setup-splits.ts4.0 KBView on GitHub
import { useCallback, useState } from 'react';
import { toast } from 'sonner';
import {
  SUPERHUMAN_TEMPLATES,
  type SplitTemplate,
} from '@/modules/threads/components/SplitInboxTabs';
import { useInboxes, type InboxConfig } from '@/modules/threads/hooks/use-inboxes';

/**
 * The templates onboarding offers.
 *
 * Only ones that apply in a single click: a concrete query, no placeholders to
 * fill (`<your-domain>`), no follow-up form, and no AI labels to provision
 * first. The full gallery — VIP lists, pipeline presets, AI-label templates —
 * stays in the inbox's own Edit dialog, where there is room to ask the follow-up
 * question. Setup is not the place to open a second form.
 */
export const SETUP_SPLIT_TEMPLATES: SplitTemplate[] = SUPERHUMAN_TEMPLATES.filter(
  (template) =>
    !!template.query &&
    !template.query.includes('<') &&
    !template.requiresUserInput &&
    !template.action &&
    !template.aiLabels?.length,
);

/** The inbox a template produced, matched on query first and name as a fallback. */
function findMatch(inboxes: InboxConfig[], template: SplitTemplate): InboxConfig | undefined {
  return (
    inboxes.find((inbox) => inbox.query?.trim() === template.query && inbox.enabled !== false) ??
    inboxes.find(
      (inbox) =>
        inbox.name.toLowerCase() === template.name.toLowerCase() && inbox.enabled !== false,
    )
  );
}

/**
 * Select/deselect sub-inbox templates.
 *
 * `pending` exists because a card has to commit the instant it is clicked, and
 * the server row it will match on does not exist yet. It holds only the names
 * whose write is still in flight — once the row lands, the derived state takes
 * over and `pending` is dropped, so a rejected write cannot leave a card stuck
 * looking applied.
 */
export function useSetupSplits(onAdded?: (inboxId: string) => void) {
  const { inboxes, addInbox, removeInbox, assertCanAddInbox } = useInboxes();
  const [pending, setPending] = useState<Record<string, 'adding' | 'removing'>>({});

  const clearPending = useCallback((name: string) => {
    setPending((current) =>
      Object.fromEntries(Object.entries(current).filter(([key]) => key !== name)),
    );
  }, []);

  const isApplied = useCallback(
    (template: SplitTemplate) => {
      const state = pending[template.name];
      if (state) return state === 'adding';
      return !!findMatch(inboxes, template);
    },
    [inboxes, pending],
  );

  /** Returns the created inbox's id when the click ADDED one, else null. */
  const toggle = useCallback(
    async (template: SplitTemplate): Promise<string | null> => {
      const match = findMatch(inboxes, template);

      if (match) {
        setPending((current) => ({ ...current, [template.name]: 'removing' }));
        try {
          await removeInbox(match.id);
        } catch (error) {
          toast.error(
            error instanceof Error ? error.message : `Couldn't remove ${template.name}`,
          );
        } finally {
          clearPending(template.name);
        }
        return null;
      }

      try {
        assertCanAddInbox(template.name);
      } catch (error) {
        toast.error(error instanceof Error ? error.message : 'Cannot add another inbox');
        return null;
      }

      setPending((current) => ({ ...current, [template.name]: 'adding' }));
      try {
        return await addInbox({
          name: template.name,
          rule: { kind: 'all_of', clauses: [] },
          lookbackDays: 30,
          query: template.query,
          alsoShowInImportant: template.alsoShowInImportantOrOther ?? false,
          // Select the new tab on the click, not on the round-trip.
          onOptimisticId: onAdded,
        });
      } catch (error) {
        toast.error(error instanceof Error ? error.message : `Couldn't add ${template.name}`);
        return null;
      } finally {
        clearPending(template.name);
      }
    },
    [inboxes, addInbox, removeInbox, assertCanAddInbox, clearPending, onAdded],
  );

  return { templates: SETUP_SPLIT_TEMPLATES, isApplied, toggle, inboxes };
}