Branch Contact Groups
Live-membership replacement for ~50 Outlook contact groups in the zero mailbox — send to groups with always-current membership, no more manual weekly rebuilds. v2.0 adds a dedicated People screen, Thursday send-splitting into two independent batches, automatic BCC-locking for external addresses, per-group history, and paste-to-replace bulk roster updates.
scrManageGroups is the only screen that changed.Older release notes have been retired from this page to keep it current and skimmable — every fix through v2.45 is already baked into the screen YAML below, so a fresh paste always reflects the latest state without needing the history. Full commit history remains in the GitHub repo.
Before You Start
- Create the four SharePoint lists below (all columns, exact names) on
<your SharePoint site URL>. - Add the two maintainer rows to BM Maintainers and your real group/member data (see the Migration Plan card below).
- Open Power Apps Studio → New blank canvas app (tablet layout).
- Add all data sources listed below (Data panel → Add data).
- Set App → OnStart using the code block below.
- Create four blank screens (name them exactly as shown), then paste each YAML in order: scrQuickContacts, scrSendEmail, scrManageGroups, scrManagePeople.
- These screens use manual X/Y positioning throughout. Anything too tall to fit on one screen (Manage Groups' member panel, Manage People's Add Person/Remove Leaver panels) is a centered lightbox instead of a scrolling section.
Data Connections
Add these in the Data panel before pasting any YAML:
| Connection | Type |
|---|---|
| 'BM Groups' | SharePoint → your site → BM Groups |
| 'BM Memberships' | SharePoint → your site → BM Memberships |
| 'BM Maintainers' | SharePoint → your site → BM Maintainers |
| 'BM History' | SharePoint → your site → BM History |
| Office365Users | Office 365 Users connector |
SharePoint Lists
List: BM Groups
| Column name | Type | Notes |
|---|---|---|
| Title | Single line of text | Built-in — the group name, e.g. "All Branch Staff". Not Required. |
| SortOrder | Number | Optional display order. Not Required. |
| ThursdayBatch | Choice | Values: None, Batch 1, Batch 2. Default None. Replaces the old single Thursday flag — the weekly send is split into two independent batches because one BCC of everyone overloads some recipients' mail servers. A group can only be in one batch (or neither), so the two sends never share a recipient. |
| Private | Yes/No | Default No. Private groups are hidden from the Send Email screen for anyone not in BM Maintainers, but are still included in Thursday batch sends. |
| OfficialSensitiveApproved | Yes/No | Default No. Shown as a pill/tag to everyone, not just maintainers — lets anyone see at a glance whether OFFICIAL-SENSITIVE material can go to this group. |
| OfficialUseOnly | Yes/No | Default No. Independent of OfficialSensitiveApproved — a group can carry either, both, or neither. |
| Notes | Multiple lines of text (plain text, not rich text) | Optional. Shown on the group card and to everyone on the Send Email screen so people can sanity-check who they're about to email. |
| ManuallySetModified | Date and Time (new) | Not Required, leave blank by default. Lets a maintainer override the "Last modified" shown on the card — useful for groups migrated from an old system where the real last-changed date predates this app. Tick "Override last modified date" when editing a group to set it; untick to go back to using SharePoint's own Modified date automatically. |
The old Active column has been removed entirely — it's not used anywhere in v2.0. If you built v1.0 already, delete that column.
"Last modified" note: the app shows SharePoint's built-in Modified date, and every formula that changes membership (add/remove/replace) also does a harmless self-Patch on the group's own Title so Modified reflects membership changes too, not just edits to the group record itself.
Versioning: List Settings → Versioning Settings → "Create a version each time you edit an item" = Yes.
Permissions: List Settings → Permissions for this list → Stop Inheriting Permissions → grant Edit to the two maintainers, Read to everyone else on the site.
List: BM Memberships
| Column name | Type | Notes |
|---|---|---|
| Title | Single line of text | Built-in — kept as a mirror of Name purely so SharePoint's own default list view still shows something meaningful. The app itself never reads Title — as of v2.34, Name is the real field the app uses everywhere (display, search, sort, rename-cascade). Not Required. |
| Name | Single line of text (new in v2.34) | The person's display name — the field the app actually reads and writes. Not Required. |
| JMC | Single line of text (new in v2.34) | Bde/Area from the standard import sheet. Optional — blank hides the field on cards. Not Required. |
| County/Contingent | Single line of text (new in v2.34) | County/District from the standard import sheet. The column name itself contains a slash, so in Power Fx it must be referenced as ThisItem.'County/Contingent' (quoted). Optional — blank hides the field on cards. Not Required. |
| Single line of text | Store lowercase. Not Required. | |
| GroupName | Single line of text | Must match a 'BM Groups'.Title value exactly. Plain text, not a Lookup column — this keeps every filter delegable and avoids the Lookup-column delegation issues this app family has hit before. Not Required. |
| Notes | Single line of text | Optional per-person note, editable from Manage Groups or Manage People. |
One row per person-per-group (deliberately denormalised — a person in 3 groups is 3 rows). This mirrors how Outlook contact groups actually work and keeps every query in the app a plain, delegable Filter. The new Manage People screen is a view over this same table grouped by email — no separate "people" list needed. JMC and County/Contingent genuinely belong to the membership row (not the person), since the same person could have different regional metadata in different groups — Manage People's per-person card picks whichever membership row happens to be found first as a reasonable approximation.
Name column, existing membership rows still have their old display name in Title and a blank Name. Backfill Name from Title once before testing the new screens, otherwise Manage People rows, member popups, search, and name sorting can appear blank even though the membership rows still exist.Versioning: enabled, same as BM Groups — gives you a native audit trail as a backstop; the app-level BM History list below is the human-readable one you'll actually use day to day.
Permissions: same unique-permission pattern as BM Groups — Edit for the two maintainers, Read for everyone else.
Pasting the standard import sheet (v2.34)
The Replace all (paste) tab inside Manage Members now recognises two formats automatically, based on how many tab-separated columns each pasted row has:
| Sheet column | Maps to |
|---|---|
| Bde / Area | JMC |
| County / District | County/Contingent |
| Rank | kept as-is, first word of Name |
| First Name | normalised to Proper Case, second part of Name |
| Surname | forced to UPPERCASE, third part of Name |
| Appointment | ignored on import |
Example: Maj / Victor / HUGHES → Name = "Maj Victor HUGHES". A header row is fine to include in the paste — it fails the email-format check and gets silently skipped, the same way blank/invalid rows always have. The old two-column Name/Email paste still works exactly as before for groups that don't have this structured data — the parser just checks the column count per pasted block and picks the matching format.
List: BM Maintainers
| Column name | Type | Notes |
|---|---|---|
| Title | Single line of text | Built-in — the maintainer's name. |
| Single line of text | Store lowercase. This is what the app checks against the signed-in user to decide whether to show Manage Groups / Manage People. |
This list exists specifically so adding a future maintainer is a one-row add to a SharePoint list, not an app edit — no formula change, no republish. Add the two starting rows yourself:
| Title | |
|---|---|
| Maintainer 1 | maintainer1@yourdomain |
| Maintainer 2 | maintainer2@yourdomain |
Permissions: anyone who should use the in-app Manage Maintainers button needs Edit access to this list, because Power Apps cannot bypass SharePoint list permissions. Everyone else only needs Read access so the app can check User().Email. The app blocks removing yourself and blocks removing the final maintainer, but SharePoint permissions are still the hard gate.
List: BM History (new in v2.0)
| Column name | Type | Notes |
|---|---|---|
| Title | Single line of text | Built-in — the human-readable event description, e.g. "Jane Doe added (jane.doe@mod.gov.uk)" or "Bulk replace via paste: 12 removed, 15 added". |
| GroupName | Single line of text | Matches a 'BM Groups'.Title value — which group this event belongs to. |
| EventType | Choice | Values: Created, Added, Removed, Edited, Replaced. Not currently displayed in the app UI but useful for future filtering/reporting. |
One row per membership/group event. The app writes to this list every time someone is added, removed, a group is created/edited, or a roster is bulk-replaced. The 🕘 history icon on each group reads this list filtered by GroupName, newest first — this is what answers "who was added or removed, and when" over time. No custom timestamp column needed; the built-in Created date is used directly.
Versioning: not needed — this list is itself the audit trail; individual rows are never edited after creation.
Permissions: same as BM Groups/BM Memberships — Edit for maintainers (the app writes here on their behalf), Read for everyone (the history icon is visible on the Send Email screen too, not just Manage Groups).
App.OnStart
Paste into App → OnStart in Power Apps Studio. This is never part of any screen's YAML. Check varModDomain matches your real domain — it's set to mod.gov.uk below.
Set(varModDomain, "mod.gov.uk");
Set(varMyEmail, Lower(User().Email));
ClearCollect(colMaintainers, 'BM Maintainers');
Set(varIsMaintainer, CountRows(Filter(colMaintainers, Lower(Email) = varMyEmail)) > 0);
ClearCollect(colGroups, AddColumns(Sort('BM Groups', SortOrder, SortOrder.Ascending), Ticked, false));
ClearCollect(colMemberships, Filter('BM Memberships', false)); ForAll(colGroups As grp, Collect(colMemberships, Filter('BM Memberships', GroupName = grp.Title)));
ClearCollect(colThursdayBatchOptions, ["None", "Batch 1", "Batch 2"]);
Set(varRecipientField, "bcc");
Set(varBccForcedByBatch, false);
Set(varBccForced, false);
Set(varPreviewGroupID, 0);
Set(varShowLightbox, false);
Set(varLightboxMode, "");
Set(varLightboxGroupID, 0);
Set(varLastMembershipRefresh, Now())
Screens
Paste each screen in order. Create a new blank screen first, name it exactly as shown, then paste.
Further Reading
Issues
Found a problem? Report it here.