This is an extension of Jeff Richmond's Default Connectors Quick Reference List recipe. He had a great idea, and I added editing capabilities so you can change connectors right from the list.

Click a connector's name and it turns into a dropdown of that opportunity's active connector group members. Pick someone and it saves right away, with a green "Saved" check to confirm. Changed rows stay highlighted so you can see what you touched. It also adds a Connector Group button next to the Edit button on each opportunity, so you can quickly add people to the list of connectors you can choose from.

Everything lives in a single HTML block with no hardcoded IDs, so it should work out of the box and be quick to set up.

How-To

  1. Create a new page under your main Connections page (Internal Homepage > People > Engagement > Connection):
    • Page Title: Default Connectors
    • Layout: Full Width
    • Icon CSS Class: fa fa-users
    • Page Route: people/connections/connectors
  2. Add an HTML Content block to the Main zone:
    • Block Title: Default Connectors
    • Enabled Lava Commands: Rock Entity, Sql
    • Cache Duration: 0 (required, or saves won't work)
  3. Paste the following into the HTML Content:
{%- assign debug = 'Global' | PageParameter:'Debug' | Downcase -%}
{%- assign rawCocId = 'Global' | PageParameter:'SaveCocId' | Trim -%}
{%- assign rawAliasId = 'Global' | PageParameter:'SaveAliasId' | Trim | Default:'0' -%}

{%- if rawCocId != empty -%}
    {%- comment -%} ===== SAVE MODE (called by the dropdown script) ===== {%- endcomment -%}
    {%- assign cocIsNumber = rawCocId | RegExMatch:'^[0-9]{1,10}$' -%}
    {%- assign aliasIsNumber = rawAliasId | RegExMatch:'^[0-9]{1,10}$' -%}

    {%- if cocIsNumber != true or aliasIsNumber != true -%}
        <div id="dc-save-result" data-status="error" data-message="Invalid parameters"></div>
    {%- else -%}
        {%- assign saveCocId = rawCocId | AsInteger -%}
        {%- assign saveAliasId = rawAliasId | AsInteger -%}

        {%- sql return:'cocRow' cocid:'{{ saveCocId }}' -%}
            SELECT TOP 1 ConnectionOpportunityId
            FROM ConnectionOpportunityCampus
            WHERE Id = TRY_CAST(@cocid AS INT)
        {%- endsql -%}
        {%- assign saveOppId = cocRow | First | Property:'ConnectionOpportunityId' | Default:0 -%}

        {%- if saveOppId == 0 -%}
            <div id="dc-save-result" data-status="error" data-message="Connection opportunity campus not found"></div>
        {%- else -%}
            {%- assign saveCanEdit = saveOppId | HasRightsTo:'Edit','Rock.Model.ConnectionOpportunity' -%}
            {%- if saveCanEdit != true -%}
                <div id="dc-save-result" data-status="error" data-message="You do not have edit rights on this opportunity"></div>
            {%- else -%}
                {%- sql return:'saveResult' cocid:'{{ saveCocId }}' aliasid:'{{ saveAliasId }}' modifiedby:'{{ CurrentPerson.PrimaryAliasId | Default:0 }}' -%}
                    SET NOCOUNT ON;
                    DECLARE @vCoc INT = TRY_CAST(@cocid AS INT);
                    DECLARE @vAlias INT = TRY_CAST(@aliasid AS INT);
                    DECLARE @vModBy INT = NULLIF(TRY_CAST(@modifiedby AS INT), 0);
                    DECLARE @vRows INT = 0;

                    BEGIN TRY
                        IF @vCoc IS NULL OR @vAlias IS NULL
                            SELECT 'error' AS Status, 'Invalid parameters' AS Message, 0 AS RowsUpdated;
                        ELSE
                        BEGIN
                            UPDATE ConnectionOpportunityCampus
                            SET DefaultConnectorPersonAliasId = NULLIF(@vAlias, 0),
                                ModifiedDateTime = GETDATE(),
                                ModifiedByPersonAliasId = @vModBy
                            WHERE Id = @vCoc;

                            SET @vRows = @@ROWCOUNT;

                            IF @vRows = 1
                                SELECT 'ok' AS Status, 'Saved' AS Message, @vRows AS RowsUpdated;
                            ELSE
                                SELECT 'error' AS Status, 'No rows were updated' AS Message, @vRows AS RowsUpdated;
                        END
                    END TRY
                    BEGIN CATCH
                        SELECT 'error' AS Status, 'Database error: ' + ERROR_MESSAGE() AS Message, 0 AS RowsUpdated;
                    END CATCH
                {%- endsql -%}
                {%- assign result = saveResult | First -%}
                <div id="dc-save-result"
                     data-status="{{ result.Status | Default:'error' | Escape }}"
                     data-rows="{{ result.RowsUpdated | Default:0 }}"
                     data-message="{{ result.Message | Default:'No response from database' | Escape }}"></div>
            {%- endif -%}
        {%- endif -%}
    {%- endif -%}

{%- else -%}
    {%- comment -%} ===== NORMAL PAGE MODE ===== {%- endcomment -%}
<div class="dc-root">

    {%- if debug == 'true' -%}
    <div class="alert alert-info">
        <strong>DEBUG</strong><br>
        Current Person: {{ CurrentPerson.FullName }} (Alias {{ CurrentPerson.PrimaryAliasId }})<br>
        Save method: same-page GET with SaveCocId / SaveAliasId. Open the console (F12) when changing a connector.
    </div>
    {%- endif -%}

    {%- connectionopportunity
        expression:'IsActive == true && ConnectionType.IsActive == true'
        select:'new (
            Id,
            Name,
            IconCssClass,
            new (
                ConnectionType.Id AS Id,
                ConnectionType.Name AS Name,
                ConnectionType.IconCssClass AS IconCssClass
            ) AS ConnectionType,
            ConnectionOpportunityCampuses.Select(new (
                Id AS ConnectionOpportunityCampusId,
                Campus.Name AS Name,
                Campus.CampusStatusValue.Value AS Status,
                DefaultConnectorPersonAliasId,
                DefaultConnectorPersonAlias.Person.NickName + " " + DefaultConnectorPersonAlias.Person.LastName AS Connector,
                Campus.Order AS Order
            )) AS Campuses
        )'
        sort:'ConnectionTypeId, Name'
        securityenabled:'false' -%}

    {%- assign connectionTypes = connectionopportunityItems | Select:'ConnectionType' | Uniq -%}

    {%- for connectionType in connectionTypes -%}
    {[ panel title:'{{ connectionType.Name }}' icon:'{{ connectionType.IconCssClass }}' type:'block' ]}

    {%- assign typeOpportunities = connectionopportunityItems
        | Where:'ConnectionType.Id',connectionType.Id
        | OrderBy:'Name' -%}

    {%- for opportunity in typeOpportunities -%}
    {%- assign rowPos = forloop.index0 | Modulo:3 | Plus:1 -%}
    {%- if rowPos == 1 -%}<div class="row">{%- endif -%}

    <div class="col-md-6 col-lg-4">

    {%- assign canEdit = opportunity.Id | HasRightsTo:'Edit','Rock.Model.ConnectionOpportunity' -%}

    {%- sql return:'connectorGroup' oppid:'{{ opportunity.Id }}' %}
        SELECT TOP 1 ocg.ConnectorGroupId
        FROM ConnectionOpportunityConnectorGroup ocg
        WHERE ocg.ConnectionOpportunityId = TRY_CAST(@oppid AS INT)
    {% endsql %}
    {%- assign connectorGroupId = connectorGroup | First | Property:'ConnectorGroupId' -%}

    {%- if canEdit -%}
    {% sql return:'people' oppid:'{{ opportunity.Id }}' %}
        SELECT
            p.PrimaryAliasId AS PersonAliasId,
            MAX(p.NickName + ' ' + p.LastName) AS Name
        FROM ConnectionOpportunityConnectorGroup ocg
        JOIN GroupMember gm ON gm.GroupId = ocg.ConnectorGroupId
        JOIN Person p ON p.Id = gm.PersonId
        WHERE ocg.ConnectionOpportunityId = TRY_CAST(@oppid AS INT)
          AND gm.GroupMemberStatus = 1
          AND p.PrimaryAliasId IS NOT NULL
        GROUP BY p.PrimaryAliasId
        ORDER BY MAX(p.LastName), MAX(p.NickName)
    {% endsql %}
    {%- endif -%}

    <div class="panel panel-default">
        <div class="panel-heading clearfix">
            {%- if canEdit -%}
            <div class="pull-right">
                {%- if connectorGroupId -%}
                <a href="/Group/{{ connectorGroupId }}" class="btn btn-xs btn-square btn-default" title="View Connector Group">
                    <i class="fa fa-fw fa-users"></i>
                </a>
                {%- endif -%}
                <a href="/people/connections/types/{{ connectionType.Id }}/opportunity/{{ opportunity.Id }}" class="btn btn-xs btn-square btn-default" title="Edit this Connection Opportunity">
                    <i class="fa fa-fw fa-pencil"></i>
                </a>
            </div>
            {%- endif -%}
            <h3 class="panel-title">
                <i class="{{ opportunity.IconCssClass }}"></i> {{ opportunity.Name | Escape }}
                {%- if debug == 'true' %} <small>(Opp {{ opportunity.Id }}, canEdit {{ canEdit }}, group {{ connectorGroupId | Default:'none' }})</small>{% endif -%}
            </h3>
        </div>

        <div class="panel-body">
            <div class="grid grid-panel">
                <table class="table grid-table table-condensed">
                    <thead>
                        <tr>
                            <th>Campus</th>
                            <th>Default Connector</th>
                        </tr>
                    </thead>
                    <tbody>
                    {%- assign sortedCampuses = opportunity.Campuses | OrderBy:'Order' -%}
                    {%- for campus in sortedCampuses -%}
                    {%- if campus.Status == 'Open' -%}
                    <tr>
                        <td>{{ campus.Name | Escape }}{% if debug == 'true' %} <small class="text-muted">(COC {{ campus.ConnectionOpportunityCampusId }})</small>{% endif %}</td>
                        <td>
                        {%- if canEdit -%}
                            {%- assign defaultAliasId = campus.DefaultConnectorPersonAliasId | Default:0 -%}
                            <span class="connector-inline">
                                <span class="dc-status" aria-live="polite"></span>
                                <button type="button" class="form-control input-sm dc-display" title="Click to change">
                                    <span class="dc-display-text">{% if defaultAliasId != 0 %}{{ campus.Connector | Escape }}{% else %}<span class="dc-placeholder">Select connector</span>{% endif %}</span>
                                    <i class="fa fa-caret-down dc-caret"></i>
                                </button>
                                <select
                                    class="form-control input-sm connector-select dc-hidden"
                                    data-coc-id="{{ campus.ConnectionOpportunityCampusId }}"
                                    data-original-alias-id="{{ defaultAliasId }}"
                                    data-current-alias-id="{{ defaultAliasId }}">
                                    {%- if defaultAliasId == 0 -%}
                                    <option value="" selected disabled hidden></option>
                                    {%- endif -%}
                                    {%- for person in people -%}
                                    <option value="{{ person.PersonAliasId }}"{% if person.PersonAliasId == defaultAliasId %} selected{% endif %}>{{ person.Name | Escape }}</option>
                                    {%- endfor -%}
                                </select>
                            </span>
                        {%- else -%}
                            {{ campus.Connector | Escape }}
                        {%- endif -%}
                        </td>
                    </tr>
                    {%- endif -%}
                    {%- endfor -%}
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    </div>

    {%- if rowPos == 3 or forloop.last -%}</div>{%- endif -%}
    {%- endfor -%}

    {[ endpanel ]}
    {%- endfor -%}
    {%- endconnectionopportunity -%}

</div>

<script>
(function () {
    var DEBUG = {% if debug == 'true' %}true{% else %}false{% endif %};
    var TIMEOUT_MS = 15000;

    function log() {
        if (DEBUG) console.log.apply(console, ["[DefaultConnectors]"].concat([].slice.call(arguments)));
    }

    function getAttr(select, name) {
        var v = select.getAttribute(name);
        if (v === null || v === undefined || v === "") return "0";
        return String(v);
    }

    function parts(el) {
        var wrap = el.closest(".connector-inline");
        return {
            wrap: wrap,
            select: wrap.querySelector(".connector-select"),
            display: wrap.querySelector(".dc-display"),
            text: wrap.querySelector(".dc-display-text"),
            status: wrap.querySelector(".dc-status")
        };
    }

    function updateChanged(p) {
        var original = getAttr(p.select, "data-original-alias-id");
        var changed = getAttr(p.select, "data-current-alias-id") !== original;
        p.display.classList.toggle("is-changed", changed);
    }

    function setDisplayText(p) {
        var opt = p.select.options[p.select.selectedIndex];
        if (opt && opt.value) {
            p.text.textContent = opt.textContent;
        } else {
            p.text.innerHTML = '<span class="dc-placeholder">Select connector</span>';
        }
    }

    function showDisplay(p) {
        p.select.classList.add("dc-hidden");
        p.display.classList.remove("dc-hidden");
    }

    function showSelect(p) {
        p.display.classList.add("dc-hidden");
        p.select.classList.remove("dc-hidden");
        p.select.focus();
        try { if (p.select.showPicker) p.select.showPicker(); } catch (err) { }
    }

    function hideStatus(s) {
        s.className = "dc-status";
        s._clear = setTimeout(function () { s.innerHTML = ""; s.title = ""; }, 250);
    }

    function showStatus(p, type, text, detail) {
        var s = p.status;
        clearTimeout(s._timer);
        clearTimeout(s._clear);
        s.className = "dc-status visible " + type;
        s.title = detail || "";
        if (type === "ok") {
            s.innerHTML = 'Saved <i class="fa fa-check"></i>';
            s._timer = setTimeout(function () { hideStatus(s); }, 1500);
        } else if (type === "saving") {
            s.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
        } else {
            s.innerHTML = '<i class="fa fa-exclamation-triangle"></i> ' + text;
            s._timer = setTimeout(function () { hideStatus(s); }, 6000);
        }
    }

    function revert(p, saved) {
        p.select.value = (saved === "0") ? "" : saved;
        setDisplayText(p);
        updateChanged(p);
    }

    log("Editable connectors on page:", document.querySelectorAll(".dc-root .connector-select").length);

    document.addEventListener("click", function (e) {
        var btn = e.target.closest ? e.target.closest(".dc-root .dc-display") : null;
        if (!btn) return;
        showSelect(parts(btn));
    });

    document.addEventListener("focusout", function (e) {
        var el = e.target;
        if (!el.classList || !el.classList.contains("connector-select") || el.disabled) return;
        showDisplay(parts(el));
    });

    document.addEventListener("keydown", function (e) {
        var el = e.target;
        if (e.key === "Escape" && el.classList && el.classList.contains("connector-select")) {
            var p = parts(el);
            revert(p, getAttr(el, "data-current-alias-id"));
            showDisplay(p);
        }
    });

    document.addEventListener("change", function (e) {
        var el = e.target;
        if (!el.classList || !el.classList.contains("connector-select")) return;

        var p = parts(el);
        var cocId = el.getAttribute("data-coc-id");
        var saved = getAttr(el, "data-current-alias-id");
        var val = String(el.value || "0");

        setDisplayText(p);
        if (val === saved) { log("No change, skipping"); showDisplay(p); return; }

        if (!/^[0-9]+$/.test(cocId) || !/^[0-9]+$/.test(val)) {
            revert(p, saved);
            showDisplay(p);
            showStatus(p, "error", "Invalid value", "Non-numeric id: coc=" + cocId + " alias=" + val);
            return;
        }

        el.disabled = true;
        showDisplay(p);
        p.display.disabled = true;
        showStatus(p, "saving");

        var u = new URL(window.location.href);
        u.searchParams.delete("Debug");
        u.searchParams.set("SaveCocId", cocId);
        u.searchParams.set("SaveAliasId", val);
        u.searchParams.set("_", Date.now());
        var url = u.toString();

        var controller = ("AbortController" in window) ? new AbortController() : null;
        var timer = controller ? setTimeout(function () { controller.abort(); }, TIMEOUT_MS) : null;

        log("Saving", { cocId: cocId, from: saved, to: val, url: url });

        fetch(url, { credentials: "same-origin", cache: "no-store", signal: controller ? controller.signal : undefined })
            .then(function (r) {
                log("Response status:", r.status, "Final URL:", r.url);
                if (r.redirected && /login/i.test(r.url)) throw new Error("Your session expired. Refresh and log in again.");
                if (!r.ok) throw new Error("Server returned HTTP " + r.status);
                return r.text();
            })
            .then(function (body) {
                var doc = new DOMParser().parseFromString(body, "text/html");
                var result = doc.getElementById("dc-save-result");
                if (!result) {
                    log("Response body (first 1000 chars):", body.substring(0, 1000));
                    throw new Error("No save result returned. Open the save URL directly to see the error.");
                }
                var status = result.getAttribute("data-status");
                var message = result.getAttribute("data-message") || "Unknown error";
                log("Save result:", status, message, "rows:", result.getAttribute("data-rows"));
                if (status !== "ok") throw new Error(message);

                el.setAttribute("data-current-alias-id", val);
                updateChanged(p);
                showStatus(p, "ok");
            })
            .catch(function (err) {
                var msg = (err && err.name === "AbortError") ? "Timed out. Try again." : (err && err.message) || "Unknown error";
                console.error("[DefaultConnectors] Save failed:", msg, url);
                revert(p, saved);
                showStatus(p, "error", "Not saved", msg);
            })
            .finally(function () {
                if (timer) clearTimeout(timer);
                el.disabled = false;
                p.display.disabled = false;
            });
    });
})();
</script>

<style>
.dc-root {
    --dc-changed-bg: rgba(74, 144, 226, 0.15);
    --dc-changed-border: #4a90e2;
    --dc-ok: #1e8e3e;
    --dc-error: #c62828;
    --dc-muted: rgba(127, 127, 127, 0.9);
}

@media (prefers-color-scheme: dark) {
    .dc-root {
        --dc-changed-bg: rgba(100, 160, 255, 0.22);
        --dc-changed-border: #6aa8ff;
        --dc-ok: #5fd08a;
        --dc-error: #ff7b7b;
    }
}

html.dark .dc-root,
html.theme-dark .dc-root,
body.dark .dc-root,
[data-theme="dark"] .dc-root,
[data-bs-theme="dark"] .dc-root {
    --dc-changed-bg: rgba(100, 160, 255, 0.22);
    --dc-changed-border: #6aa8ff;
    --dc-ok: #5fd08a;
    --dc-error: #ff7b7b;
}

html.light .dc-root,
[data-theme="light"] .dc-root,
[data-bs-theme="light"] .dc-root {
    --dc-changed-bg: rgba(74, 144, 226, 0.15);
    --dc-changed-border: #4a90e2;
    --dc-ok: #1e8e3e;
    --dc-error: #c62828;
}

.dc-root .panel-heading { padding-left: 12px; }
.dc-root .panel-block > .panel-heading .panel-title i { display: inline-block; }

.dc-root .dc-hidden { display: none !important; }

.dc-root .dc-display {
    display: flex;
    align-items: center;
    justify-content: space-between;
    text-align: left;
    cursor: pointer;
    user-select: text;
}

.dc-root .dc-display-text {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dc-root .dc-placeholder { color: var(--dc-muted); font-style: italic; }
.dc-root .dc-caret { margin-left: 6px; opacity: 0.6; }

.dc-root .dc-display.is-changed {
    background-color: var(--dc-changed-bg);
    border-color: var(--dc-changed-border);
}

.dc-root .dc-display:disabled,
.dc-root .connector-select:disabled { opacity: 0.7; cursor: wait; }

.dc-root span.connector-inline {
    position: relative;
    display: block;
    width: 100%;
}

.dc-root .dc-status {
    position: absolute;
    right: calc(100% + 8px);
    top: 50%;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
    font-size: 12px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

.dc-root .dc-status.visible { opacity: 1; }
.dc-root .dc-status.ok { color: var(--dc-ok); }
.dc-root .dc-status.error { color: var(--dc-error); cursor: help; pointer-events: auto; }
.dc-root .dc-status.saving { color: var(--dc-muted); }
</style>
{%- endif -%}

Troubleshooting

Add ?Debug=true to the page URL to show IDs and permissions on the page and log every save to the browser console (F12). If a save fails, hover the red "Not saved" message for the reason.

Follow Up

Massive thanks to Jeff for the original recipe. Leave a comment below if you have questions or run into any issues.

If your church could use help with Rock workflows, dashboards, or tools like this one, I do some contract work on the side. You can find me at hadleysutherland.netlify.app.

Thank you so much!

Change Log

  • 2026-09-21: Initial version