Library & File Plan Manager
SharePoint permissions map (file-plan style, grouped by team) + an admin screen that creates document libraries correctly and registers them automatically, so nothing gets forgotten again.
Email a CSV of the library list: one button on Manage Libraries (v2.10)
Adds one button, Email CSV (current view), to your working Manage Libraries screen. Press it and a CSV file arrives in your inbox as an attachment, ready to forward. It contains exactly the libraries currently showing on screen, so with no filters set it is every tracked library, and with a filter such as “0 files” it is just those. Admin only: the button and its status line only appear for the emails in your varIsAdmin list, the same rule as the rest of this screen. Nothing is stored anywhere: no new list, no new library, no status system, and your stats flow and v2.9 screen are untouched.
Why email and not a straight browser download: Power Apps can only download a file that already exists at a real web address, and your own test confirmed it cannot turn text it builds on screen into a download. Email is the smallest route that needs no storage.
Columns: Library, Library URL, Team, Sub-Team, Limited (flagged or named _LTD/(LTD)), Created (dd/mm/yyyy), File count (blank if never counted, never shown as 0), Empty (Yes/No/Unknown), Review (Yes only when zero files and created at least six calendar months ago). Sorted by Team, Sub-Team, then name.
Apply these steps in order
- Download EmailLibraryCsv_flow_import.zip. Power Automate → My flows → Import → Import Package (Legacy). Leave the flow on Create as new, choose your Office 365 Outlook connection when asked, then import. It is a brand new flow called FP-EmailLibraryCsv(App); it does not replace any existing flow.
- Open the new flow in My flows and make sure it is turned on. Imported flows can arrive switched off, which is what caused the earlier
WorkflowTriggerIsNotEnablederror. - In Power Apps Studio, open the Data pane → Add data → search for FP-EmailLibraryCsv(App) and add it. Do this before pasting the screen, otherwise the new button shows an error.
- Open the v2.10 full-screen copy page, copy its YAML, delete
scrManageLibraries, then paste the whole copied YAML as you normally do. No App.OnStart change is needed. - Press Play and click Email CSV (current view) (right-hand end of the row just above the list). The button greys out and reads “Sending...” with a line beside it saying who it is going to. After a few seconds you get a green banner, and a status line stays on screen beside the button, for example “Sent to you@... at 10:42 (212 libraries)”, so you can still see it after the banner fades. If it fails you get a red banner and a red status line instead. The email should arrive within a minute or two. Open the attachment in Excel and check a library you know has zero files shows Empty = Yes.
base64ToBinary(base64(concat(uriComponentToString('%EF%BB%BF'), triggerBody()?['CsvText'])))
Change a library's URL: one-library test first
A new flow, FP-RenameLibraryUrl(App), changes the URL of one tracked library by moving its root folder to the new name, then updates that library's row in LibraryFilePlan so the app stays in step. It is a dry run unless you type RENAME, so you can see exactly what it would do first. It does not touch the library's display name, contents or permissions. There is no app button for it yet: this step is only to prove the rename works in your tenant before anything real is renamed.
Try it on a throwaway library
- In the app, create a throwaway library (for example
14_test_oldname) so it is tracked in the file plan. - Download RenameLibraryUrl_flow_import.zip. Power Automate → My flows → Import → Import Package (Legacy), leave it on Create as new, choose your SharePoint connection, import. Make sure the new flow is turned on.
- Open the flow → Test → Manually → Test. Fill in Current library URL as
/teams/1598/14_test_oldnameand New URL name as14_test_newname. Leave the third box empty and run it. The run should succeed and its last step should say it was a dry run, showing the new URL it would use. - Run it again with the third box set to exactly
RENAME. This is the real rename. - Check: the library opens at the new address, the old address no longer works, and after Reload saved stats on Manage Libraries (or reopening the app) the library shows the new URL. Then press Refresh Index on Group Search so permission rows pick up the new address.
- Tell me what happened, including the exact error text if any step failed.
Why there is no on-screen flag yet
I tested the idea of flagging libraries whose URL does not match their name against your real list of 574. Your URLs follow a numbering scheme (for example 01_01_8_ACOS_WiP_LTD), so a plain name-versus-URL check would flag almost every library. Most of those are deliberate. The ones that are genuinely different fall into two groups: about 86 where the URL is only a code (for example 01_01_7), and about 106 where the words in the URL are different or cut off (for example 05_MedicalCentre_ManagementDefence_Awarding_Organi). Several libraries in the same team also share the same display name, so a name-only URL would clash. The flag waits until the naming rule is agreed.
Restore library housekeeping: Manage Libraries v2.9
20 October 2026: the latest Vault upload has no stats controls. This full-screen update restores them on that exact uploaded screen. The original stats flow was confirmed working on 26 September; its SharePoint ItemCount includes folders. This update adds a separate, file-only count, including documents inside nested folders.
Result: every expanded library row shows its actual creation date and file count. The entire row turns pale red only when there are zero files AND the library was created at least six calendar months ago. A populated library is never flagged merely for being old. The checkbox and selection border still show selection. Filters: all counts, zero, five or fewer, ten or fewer, or missing stats; combined with any age, six months or older, or one year or older. Search and Team filters continue to work alongside them.
Apply these steps in order
- On the main site, open the LibraryFilePlan list settings. Check that
LibraryCreatedalready exists as Date and Time, andLibraryItemCountas Number. Reuse them if present; create them with those exact names if absent.LibraryCreatedis the library's real creation date, not the list row's built-inCreated. - Create
LibraryFileCountas a Number column, zero decimal places, not required, no default value. Leave existing rows blank. Do not default it to zero: blank means not checked, while zero is a measured empty library. - Download RebuildLibraryStats_FileCounts_flow_import.zip. Power Automate → My flows → Import → Import Package (Legacy). Set the flow resource to Update the existing FP - Rebuild Library Stats, select your existing SharePoint connection, then import. The package retains the real exported resource ID. If Update is unavailable, stop rather than creating a duplicate.
- Run FP - Rebuild Library Stats in Power Automate and wait for a successful completed run. Leave the optional SiteUrl input blank. This version always scans the main site and CCF and writes stats only to already tracked rows in the main site's LibraryFilePlan. Untracked libraries still need Backfill first. The exact count scans item IDs/types in pages and may take longer than the old metadata-only scan.
- In Power Apps Studio, refresh the LibraryFilePlan data source so Studio sees
LibraryFileCount. Open the v2.9 full-screen copy page, copy its YAML, deletescrManageLibraries, then paste the whole copied YAML. Studio recreates the screen with its name. No App.OnStart change or new app flow connection is needed. - Open Manage Libraries and click Reload saved stats. Expand a Team/Sub-Team, or choose 0 files and 6+ months old to expand matching groups automatically. Review the counts and dates before publishing the app.
tools/check_housekeeping.py. This revised flow has not yet been imported or run in the tenant, and this screen has not yet been pasted or visually tested in Studio.These are saved snapshots, not continuously live counts. After documents change, rerun the flow to completion and click Reload saved stats. Missing values say not checked, never zero. A failed or incomplete paged count does not overwrite that library with a partial count; check flow run history before relying on the latest scan. Nothing is automatically deleted or persistently flagged in SharePoint.
The earlier multi-site checklist below is historical work with mixed confirmation status. For this housekeeping update, follow the six steps above.
Everything Left To Do — Work Through This Whole Section, Top To Bottom
Every single outstanding item — SharePoint changes, flow edits, and screen pastes — gathered into one place in the order to do them, so nothing needs hunting for elsewhere in the guide. Every zip file, code block, and link needed sits right next to the step that needs it. Once you've worked through everything below, this whole section shrinks back down to nothing.
1. Add the new SiteUrl column to PermissionIndex
Two-minute prerequisite for items 4 and 8 below — without it, the multi-site flow edits have nowhere to write the site each library actually lives on.
- SharePoint → your main site → PermissionIndex list → list settings (gear icon → List settings) → Create column.
- Name:
SiteUrl. Type: Single line of text. Leave Not Required. - Save. Nothing further needed here — the flow edit in item 4 below is what actually starts writing values into it.
2. Paste scrAdminCreateLibrary v1.13
Self-contained, no flow changes needed — safe to do any time, independent of everything else in this list. Auto-prefixes the Technical Name with the selected Team's own number (Team 4 → 04_), plus an optional "Extra prefix" field (Team 4 + SOP + extra field 04_03 → final name 04_04_03_SOP). v1.13 fixes real bugs found testing v1.12: the prefix now applies to the Display Name too, not just the Technical Name (so the library's visible title matches its URL, not just "Test" while the URL says 14_Test); the Team dropdown and every text field now have a visible white fill and border, instead of blending into the grey page background; the Sub-Team label no longer wraps and overlaps the field below it; and the queue list now shows the actual computed result under each entry (e.g. 14_04_03_SOP, or "registers <existing title>" for a Register-Only row), not just the team name.
- Power Apps Studio → open scrAdminCreateLibrary → delete it.
- New screen (blank) → open the link above → Copy YAML to Clipboard → paste into the tree view.
- Rename the new screen back to
scrAdminCreateLibrary.
LibraryFilePlan only ever exists on the main site, never on CCF, but the actions that read/write it were wrongly pointed at whichever site was currently being scanned — so the moment the CCF pass started, every single library failed with a 404 looking for a list that isn't there. Item 4 (Rebuild Permission Index) was never affected. Fixed and re-validated in the zips below — re-import the same way, pick Update again.3. Backfill File Plan — BackfillFilePlan_MultiSite_flow_import.zip
Built from a real export of your current flow (Aug 2026), with the CCF site loop added and the long-standing Pretty Name bug fixed (the flow's insert was silently writing to DisplayName, a field that doesn't exist on this list, instead of PrettyName). Updated 30 Sep 2026 — see the callout above.
- Power Automate → My flows → the "..." next to Backfill File Plan → Import → Import Package (Legacy).
- Upload the zip above. On the resource mapping screen, the flow row should show Update as an option (or let you pick the existing Backfill File Plan flow) — pick that, not "Create as new." The SharePoint connection row should already show your existing connection.
- Import. Nothing to refresh in Power Apps afterward — this flow is only ever run manually from Power Automate, it's not a data source in the app.
4. FP - Rebuild Permission Index — RebuildPermissionIndex_MultiSite_flow_import.zip
Built from a real export of your current flow (25 Sep 2026, already has the $top=5000 pagination fix). Adds the CCF site loop (the "wipe everything" step still runs once, before the loop, so a CCF run can never delete the main site's rows) and folds in the LibraryId field you added by hand in Designer for the View Permissions button — that fix is already baked into this zip, you don't need to re-add it.
- Power Automate → My flows → "..." next to FP - Rebuild Permission Index → Import → Import Package (Legacy).
- Upload the zip above. Pick Update/the existing flow on the mapping screen, same as item 3.
- Import. Then in Power Apps: Data panel → find this flow → Refresh.
5. FP - Rebuild Library Stats — RebuildLibraryStats_MultiSite_flow_import.zip
Adds the CCF site loop. Rebuilt 30 Sep 2026 from a real export of your current live flow (thanks for sending it over) — the first version of this zip was hand-built from scratch and Power Automate couldn't match it to the existing flow, only offering "Create as new." This version carries the flow's real ID, so it should now offer Update like the others. Updated again, same day — had the same LibraryFilePlan-site bug as Backfill (see the callout above item 3), now fixed.
- Power Automate → My flows → "..." next to FP - Rebuild Library Stats → Import → Import Package (Legacy).
- Upload the zip above. Pick Update/the existing flow on the mapping screen.
- Import. Then in Power Apps: Data panel → find this flow → Refresh.
6. Create Library (App) — CreateLibrary_MultiSite_flow_import.zip
Built from a real export of your current flow (Aug 2026). Adds both the "Register an existing library" path (needed for scrAdminCreateLibrary's toggle) and CCF routing (every site-address field now sends Team 09_Combined Cadet Force to the CCF sub-site, everything else to the main site or whatever SiteUrl is passed in). Built without Set/Initialize Variable actions — instead of converging the two paths into one shared Response at the end, each path (create new / register existing / already registered) now ends with its own Response action, so nothing changes about what Power Apps receives back, just how the flow gets there internally. Updated 30 Sep 2026 — the actions that write to LibraryFilePlan (both the normal create path and the register path) had the same site-routing bug as Backfill (see the callout above item 3): they were following the CCF-routing logic instead of always targeting the main site, which would have made creating any CCF library fail at the very last step, after the real document library and its permissions were already set up. Never actually tested live before this was caught, so no real CCF library was affected — but re-import before testing CCF creation.
- Power Automate → My flows → "..." next to Create Library (App) → Import → Import Package (Legacy).
- Upload the zip above. Pick Update/the existing flow on the mapping screen, same as item 3.
- Import. Before refreshing it in Power Apps, test all three paths in the flow's own Test panel:
- Normal create,
RegisterOnlyoff, Team a normal one (e.g.Test) — confirm the library lands on the main site as before. - Normal create, Team
09_Combined Cadet Force— confirm it lands on the CCF sub-site instead. RegisterOnlyon, a real existing (untracked) library's title — confirm it gets aLibraryFilePlanrow without creating a new document library.
LibraryFilePlanrows once confirmed, then re-check Library Settings → Permissions on the Limited test library exactly as before (onlySG_Cadets-Admin-Ltd_MEMandSG_IHUB, no extras). - Normal create,
- Then in Power Apps: Data panel → find this flow → Refresh.
7. Paste App.OnStart v3.18
Select the App object (top of the tree, not any screen) → OnStart property → select all → paste this in full, replacing everything. v3.18 adds Team/Sub-Team grouping to Group Search's own data (new colGSTeamGroups/colGSSubTeamGroups/colGSExpandedTeams/colGSExpandedSubTeams), needed by item 8 below — paste this one first.
Set(varTenantHost, "https://modgovuk.sharepoint.com");
ClearCollect(colFilePlan, 'LibraryFilePlan');
ClearCollect(
colFilePlanFiltered,
AddColumns(colFilePlan, TeamName, If(IsBlank(Team.Value) || Team.Value = "", "Uncategorised", Team.Value), SubTeamName, Coalesce(SubTeam.Value, ""), EffectiveName, If(IsBlank(PrettyName) || PrettyName = "", Title, PrettyName))
);
ClearCollect(
colTeamGroups,
Sort(
AddColumns(
GroupBy(colFilePlanFiltered, TeamName, GroupedItems),
ItemCount, CountRows(GroupedItems),
TeamSortKey, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName)
),
TeamSortKey,
SortOrder.Ascending
)
);
ClearCollect(
colSubTeamGroups,
AddColumns(
GroupBy(
Filter(colFilePlanFiltered, !IsBlank(SubTeamName) && SubTeamName <> ""),
TeamName, SubTeamName, GroupedItems
),
ItemCount, CountRows(GroupedItems)
)
);
Clear(colFlatSource);
Collect(colFlatSource, AddColumns(colTeamGroups, RowType, "Team", SortKey, TeamSortKey, SubSortKey, "", RankInGroup, 0));
Collect(colFlatSource, AddColumns(colSubTeamGroups, RowType, "SubTeam", SortKey, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName), SubSortKey, SubTeamName, RankInGroup, 1));
Collect(colFlatSource, AddColumns(colFilePlanFiltered, RowType, "Library", SortKey, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName), SubSortKey, If(IsBlank(SubTeamName) || SubTeamName = "", "zzz_none", SubTeamName), RankInGroup, If(IsBlank(SubTeamName) || SubTeamName = "", 1, 2)));
ClearCollect(colExpandedKeys, Filter(Table({TeamName: "seed"}), false));
ClearCollect(colExpandedSubTeams, Filter(Table({SubTeamKey: "seed"}), false));
Set(varIsAdmin, Lower(User().Email) in ["allan.hamilton775@mod.gov.uk", "dan.smith391@mod.gov.uk"]);
ClearCollect(colQueue, Filter(Table({TechnicalName: "seed", DisplayName: "", Team: "", SubTeam: "", IsLimited: false, Categories: "", RegisterOnly: false, ExistingLibraryTitle: ""}), false));
Set(varQueueRunning, false);
Set(varQueueTotal, 0);
Set(varQueueDone, 0);
Set(varIndexRunError, false);
Set(varIndexRefreshing, false);
ClearCollect(colPermissionIndex, Filter('PermissionIndex', PermissionLevel <> "Limited Access" && PrincipalType <> "User"));
ClearCollect(colPermissionFiltered, colPermissionIndex);
ClearCollect(colLibraryGroups, Sort(AddColumns(GroupBy(colPermissionFiltered, LibraryUrl, LibraryTitle, GroupedItems), ItemCount, CountRows(GroupedItems), FlagSaysLimited, With({_lu: LibraryUrl}, Coalesce(LookUp(colFilePlan, LibraryUrl = _lu, IsLimited), false)), LibraryId, With({_lu: LibraryUrl}, LookUp(colPermissionFiltered, LibraryUrl = _lu, LibraryId)), SiteUrl, With({_lu: LibraryUrl}, Coalesce(LookUp(colPermissionFiltered, LibraryUrl = _lu, SiteUrl), "https://modgovuk.sharepoint.com/teams/1598")), TeamName, With({_lu: LibraryUrl}, With({_t: LookUp(colFilePlan, LibraryUrl = _lu, Team.Value)}, If(IsBlank(_t) || _t = "", "Uncategorised", _t))), SubTeamName, With({_lu: LibraryUrl}, Coalesce(LookUp(colFilePlan, LibraryUrl = _lu, SubTeam.Value), ""))), LibraryTitle, SortOrder.Ascending));
ClearCollect(colGSTeamGroups, Sort(AddColumns(GroupBy(colLibraryGroups, TeamName, GroupedItems), ItemCount, CountRows(GroupedItems), TeamSortKey, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName)), TeamSortKey, SortOrder.Ascending));
ClearCollect(colGSSubTeamGroups, AddColumns(GroupBy(Filter(colLibraryGroups, !IsBlank(SubTeamName) && SubTeamName <> ""), TeamName, SubTeamName, GroupedItems), ItemCount, CountRows(GroupedItems)));
Clear(colPermFlatSource);
Collect(colPermFlatSource, AddColumns(colGSTeamGroups, RowType, "Team", GSTeamSort, TeamSortKey, SubSortKey, "", TeamRank, 0, LibSortKey, "", RankInGroup, 0, GroupSortKey, "", SubTeamName, ""));
Collect(colPermFlatSource, AddColumns(colGSSubTeamGroups, RowType, "SubTeam", GSTeamSort, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName), SubSortKey, SubTeamName, TeamRank, 1, LibSortKey, "", RankInGroup, 0, GroupSortKey, ""));
Collect(colPermFlatSource, AddColumns(colLibraryGroups, RowType, "Library", GSTeamSort, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName), SubSortKey, If(IsBlank(SubTeamName) || SubTeamName = "", "", SubTeamName), TeamRank, If(IsBlank(SubTeamName) || SubTeamName = "", 1, 2), LibSortKey, LibraryUrl, RankInGroup, 0, GroupSortKey, "", DirectGroupCount, With({_lu: LibraryUrl}, CountRows(Filter(colPermissionFiltered, LibraryUrl = _lu && (IsBlank(NestedUnderGroup) || NestedUnderGroup = ""))))));
Collect(colPermFlatSource, AddColumns(Filter(colPermissionFiltered, IsBlank(NestedUnderGroup) || NestedUnderGroup = ""), RowType, "Group", GSTeamSort, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName))), SubSortKey, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, If(IsBlank(SubTeamName) || SubTeamName = "", "", SubTeamName))), TeamRank, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, If(IsBlank(SubTeamName) || SubTeamName = "", 1, 2))), TeamName, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, TeamName)), SubTeamName, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, SubTeamName)), LibSortKey, LibraryUrl, RankInGroup, 1, GroupSortKey, GroupName, MemberCount, With({_gn: GroupName, _lu: LibraryUrl}, CountRows(Filter(colPermissionFiltered, NestedUnderGroup = _gn && LibraryUrl = _lu)))));
Collect(colPermFlatSource, AddColumns(Filter(colPermissionFiltered, !IsBlank(NestedUnderGroup) && NestedUnderGroup <> ""), RowType, "Member", GSTeamSort, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, If(TeamName = "Uncategorised", "zzz_" & TeamName, TeamName))), SubSortKey, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, If(IsBlank(SubTeamName) || SubTeamName = "", "", SubTeamName))), TeamRank, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, If(IsBlank(SubTeamName) || SubTeamName = "", 1, 2))), TeamName, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, TeamName)), SubTeamName, With({_lu: LibraryUrl}, LookUp(colLibraryGroups, LibraryUrl = _lu, SubTeamName)), LibSortKey, LibraryUrl, RankInGroup, 2, GroupSortKey, NestedUnderGroup));
ClearCollect(colExpandedLibs, Filter(Table({LibraryUrl: "seed"}), false));
ClearCollect(colExpandedGroups, Filter(Table({GroupKey: "seed"}), false));
ClearCollect(colGSExpandedTeams, Filter(Table({TeamName: "seed"}), false));
ClearCollect(colGSExpandedSubTeams, Filter(Table({SubTeamKey: "seed"}), false));
ClearCollect(colSelectedLibraries, Filter(Table({LibraryUrl: "seed"}), false));
ClearCollect(colManageFiltered, colFilePlanFiltered);
ClearCollect(colEditQueue, Filter(colFilePlanFiltered, false));
ClearCollect(colDeleteSkipped, Filter(Table({LibraryUrl: "seed", Title: "seed", ItemCount: 0, Deleted: false}), false));
Set(varDeleteBusy, false);
Set(varDeleteStatus, "");
Set(varConfirmOverrideVisible, false);
Set(varDeletedCount, 0);
Set(varEditSaving, false);
Set(varEditSaveStatus, "")
EffectiveName is PrettyName when filled in, otherwise falls back to Title. varIsAdmin gates the Admin screen — edit the email list directly in this line if who's authorized ever changes.8. Paste scrGroupSearch v3.16
v3.15 (Team/Sub-Team grouping, the Group-row View Permissions button, three-state badge) is confirmed pasted and live, per Allan, 01 Oct 2026. v3.16 is a follow-up fix on top of that, needed because the Team/Sub-Team/Library row sizes and indents didn't read as a hierarchy — Library text was bigger than Sub-Team text, and the Library row's own icon/count badge didn't shift inward when nested under a Sub-Team (only its title text did), so nested libraries looked misaligned rather than indented. Fixed: Sub-Team text now an explicit 13pt, Library text dropped from 14pt to 12pt (so size now steps down Team 15 → Sub-Team 13 → Library 12, matching depth), and the Library row's count badge + chevron icon now shift inward together with the title whenever a Sub-Team is present, instead of only the title moving.
- Power Apps Studio → open scrGroupSearch → delete it.
- New screen (blank) → open the link above → Copy YAML to Clipboard → paste into the tree view.
- Rename the new screen back to
scrGroupSearch.
9. scrManageLibraries v2.8 — confirmed pasted, per Allan 01 Oct 2026
Adds the third badge state that Group Search structurally can't show: a library flagged or named Limited whose permissions are still fully inherited — a real inconsistency worth catching, shown as a red triangle, distinct from the red padlock (genuinely Limited) and the amber pencil (permissions customised, nothing marks it Limited). A new legend line under the screen title explains all three. Nothing outstanding here — the paste page stays up for reference, or for re-pasting from scratch if the screen ever needs rebuilding.
10. FP-DeleteLibraries(App) — optional, low priority
Not urgent — deprioritised by Allan ("deleting libraries matters far less than creating them"), included here so it's not lost, do it whenever. This flow's Response action has no declared schema, so Power Apps can't infer the shape of anything inside Results until the flow has actually produced one real sample to learn from — every field shows as type "Error" until then, which is exactly the error scrManageLibraries hit the first time this flow was ever called. DeleteLibraries_SchemaFix_flow_import.zip adds the schema directly — no manual clicking required.
- Power Automate → My flows → "..." next to FP-DeleteLibraries(App) → Import → Import Package (Legacy).
- Upload the zip above. Pick Update/the existing flow on the mapping screen if offered.
- Import. Then in Power Apps: Data panel → find FP-DeleteLibraries(App) → Refresh.
- Test on scrManageLibraries with a real throwaway empty library before trusting it for anything real.
{
"Results": [
{ "LibraryUrl": "/teams/1598/04_Training_ZZTest", "Title": "ZZTest", "ItemCount": 0, "Deleted": true }
]
}
- SharePoint lists — create
LibraryFilePlanandPermissionIndexvia the CSV templates (fastest — creates every column for you), or manually per the schema tables. Column isPrettyName, notDisplayName— SharePoint silently drops writes to that name on this list. - Flow field fix — in Create Library (App), edit
Create_FilePlan_Row's Body so it writesPrettyNameinstead ofDisplayName. One line, no re-import. - Flow field fix (missing libraries) — in Rebuild Permission Index, add
&$top=5000toGet_Unique_Perm_Libraries's Uri so it stops capping out around 20-30 libraries. One line, no re-import. - Backfill name fix (optional) — same
DisplayName→PrettyNamebug as above, in Backfill File Plan. Only matters if you ever run Backfill again. - Register Existing Library (optional) — adds a second path through Create Library (App) for registering a library that already exists, without creating a new one. Needed for scrAdminCreateLibrary v1.9's new toggle.
- Import all four flows — Create Library (App), Backfill File Plan, Rebuild Permission Index, Manage Library Permissions. Test Create Library (App) manually before touching Power Apps.
- Data connections + App.OnStart — add both SharePoint lists and the flow as data sources, paste App.OnStart in full.
- All three live screens — delete and paste fresh (screens have had controls added since first built, so a partial paste over the top won't apply cleanly). Then scrManageLibraries and scrEditLibraries — brand new, paste as new blank screens, nothing to delete first. scrManagePermissions remains just a design, not built — see the Screens section.
- Navigation bar — select each screen in the Tree view, paste that screen's snippet as an add-on (not a full replacement). Nudge existing top controls down afterward to clear the bar.
1. SharePoint List: LibraryFilePlan
Location: your main site — https://modgovuk.sharepoint.com/teams/1598/.
LibraryFilePlan_list_template.csv — download this, then Site Contents → New → List → From CSV (or Excel) → upload it. SharePoint reads the embedded schema and creates every column with the correct type and Team's choices already filled in — no manual column-by-column setup. Delete the single placeholder row afterward, and rename the list to exactly LibraryFilePlan if it doesn't pick that up from the filename.
| Column name (exact, no spaces) | Type | Notes |
|---|---|---|
| Title | Single line of text | Built-in — the flow writes the library's pretty display name into it automatically |
| LibraryUrl | Single line of text | The real join key — a library's server-relative URL, survives display-name renames |
| Team | Choice | Top-level file-plan grouping. Fixed set — type in your known team names as the choices, no "Allow fill-in values" |
| SubTeam | Choice | Optional nested grouping. Tick "Allow fill-in values" — same pattern as the CAT column, so new sub-teams can be added on the fly |
| PrettyName | Single line of text | Optional override; falls back to live library Title if blank. Not "DisplayName" — SharePoint silently drops writes to a column with that exact name on this list (collides with a reserved sub-property on the two Person-or-Group columns, Created By/Modified By) |
| SiteUrl | Single line of text | Which of your sites the library is on |
| SortOrder | Number | Optional — controls order within a group |
| IsLimited | Yes/No | True if the library was created as Limited — lets you set conditional formatting on the list view to colour-code Limited rows |
| LibraryCreated | Date and Time | New, added 26 Sep 2026 for the housekeeping/audit view on scrManageLibraries. Snapshot of the real SharePoint library's own creation date — deliberately named away from the built-in reserved Created column every list already has (that one records when this row was added, a different thing entirely). Populated by FP - Rebuild Library Stats, see its own section below — blank until that's been run at least once. |
| LibraryItemCount | Number | New, added 26 Sep 2026, same reason. How many items are in the real library right now — the whole point is spotting old libraries with zero files, so nobody's actually using them. Same population/staleness rules as LibraryCreated. |
_x0020_ internally, and the flows' requests need the plain names to match exactly.SharePoint List: PermissionIndex
Location: your main site, same as LibraryFilePlan.
PermissionIndex_list_template.csv — same idea: Site Contents → New → List → From CSV → upload, delete the placeholder row, rename to exactly PermissionIndex if needed.
| Column name (exact, no spaces) | Type | Notes |
|---|---|---|
| Title | Single line of text | Built-in — set to the same value as GroupName, so the list is readable at a glance |
| LibraryUrl | Single line of text | Which library this permission row belongs to — joins back to LibraryFilePlan.LibraryUrl |
| LibraryTitle | Single line of text | Display name, so search results don't need a lookup back to LibraryFilePlan |
| GroupName | Single line of text | The actual SG_/DG_ group name (or username) this row represents — what the group-search screen filters on |
| PrincipalType | Single line of text | "SharePoint Group", "Security Group", or "User" |
| PermissionLevel | Single line of text | e.g. "Full Control", "Contribute", "Read" |
| NestedUnderGroup | Single line of text | Blank for a group/user granted directly on the library. If this row is a DG_ AD group found nested inside an SG_ group, this holds that SG_ group's name |
| LibraryId | Single line of text | The library's SharePoint List ID (GUID). Powers the View Permissions button on scrGroupSearch. |
| SiteUrl | Single line of text | New, added 29 Sep 2026 for multi-site support (section 2g). Which real site the library actually lives on — needed so View Permissions opens the correct site's permissions page, not always the main one. |
LibraryFilePlan.2. Flow Field Fix: Create_FilePlan_Row
In Power Automate, open Create Library (App) → Edit → click Create_FilePlan_Row to expand it → click into its Body box → select all, delete → paste this in as the replacement → Save. Existing action, existing field, no re-import needed.
{
"__metadata": { "type": "SP.Data.LibraryFilePlanListItem" },
"Title": "@{triggerBody()?['DisplayName']}",
"LibraryUrl": "@{body('Get_New_Library_Url')?['d']?['RootFolder']?['ServerRelativeUrl']}",
"Team": "@{triggerBody()?['Team']}",
"SubTeam": "@{triggerBody()?['SubTeam']}",
"PrettyName": "@{triggerBody()?['DisplayName']}",
"SiteUrl": "@{triggerBody()?['SiteUrl']}",
"IsLimited": @{if(equals(triggerBody()?['IsLimited'], true), 'true', 'false')}
}
DisplayName → PrettyName). The value is still populated from triggerBody()?['DisplayName'] — that's the Power Apps trigger's own parameter name, unrelated to the SharePoint column, and it hasn't changed.2b. Flow Field Fix: Get_Unique_Perm_Libraries (missing libraries bug)
Cause: this action asks SharePoint for every library with unique permissions, but never told it how many to return. SharePoint's _api/web/lists endpoint defaults to a page size of around 100 items per response and doesn't automatically continue to the next page — with enough lists on the site (including hidden/system ones counted before the filter narrows them down), the real Limited libraries near the end of that list never got reached, so the index silently stopped at whatever fit in the first page.
In Power Automate, open Rebuild Permission Index → Edit → click Get_Unique_Perm_Libraries to expand it → click into its Uri box → add &$top=5000 to the end → Save. Existing action, one field, no re-import needed.
_api/web/lists?$filter=BaseTemplate eq 101 and Hidden eq false and HasUniqueRoleAssignments eq true&$select=Title,Id,RootFolder/ServerRelativeUrl&$expand=RootFolder&$top=5000
2c. Flow Field Fix: Backfill File Plan (same DisplayName bug as 2)
LibraryFilePlan right now. For those, export the list, fill in Pretty Name yourself (AI-assisted or by hand), re-import, then refresh colFilePlan in the app.2d. Flow Surgery: Create Library (App) — Register Existing Library
2e. Flow Field Fix + New Column: Open Permissions Link
Want: a button on scrGroupSearch's library rows that opens that specific library's real SharePoint permissions page directly, in a new tab — no more hunting for it manually.
How it's possible: SharePoint's classic "Permissions for this document library" page is {site}/_layouts/15/user.aspx?obj={GUID}&List={GUID}, where the GUID is that specific list's internal ID. Confirmed 26 Sep 2026 from a real URL Allan copied out of Studio. Nothing we currently store has that GUID — LibraryFilePlan and PermissionIndex both only have the server-relative URL — but FP - Rebuild Permission Index already fetches it for every library it scans (its Get_Unique_Perm_Libraries step selects Id alongside Title and the URL), it just never saves it anywhere. Cheapest fix: capture it.
- Add one column to
PermissionIndex: SharePoint list settings → Create column →LibraryId, type Single line of text, Not Required. - Fix
Select_Librariesfirst. This action runs right afterGet_Unique_Perm_Librariesand reshapes its results down to justTitleandUrlbeforeApply_to_each_Libraryever sees them —Idgets fetched from SharePoint one step earlier, then thrown away here. OpenSelect_Libraries→ its Select box already has Title and Url mapped → add a third field:Id → @item()?['Id']
- Edit the flow. Power Automate → FP - Rebuild Permission Index → Edit → find both
Create_Index_RowandCreate_Nested_Index_Row(insideApply_to_each_Assignment→If_Not_System_Group, and inside the nestedIf_SharePoint_Group→Apply_to_each_Nested_Member) → click into each one's Body box → add one new line before the closing brace:Same expression in both actions —"LibraryId": "@{items('Apply_to_each_Library')?['Id']}"Apply_to_each_Libraryis the outer loop both actions already run inside, so itsIdis available in each without any new lookup, once step 2 above has put it there. - Save the flow.
- Refresh the
PermissionIndexdata source in Power Apps too, not just the flow. Adding a column directly to a SharePoint list doesn't make the app aware of it — Power Apps caches a data source's column schema separately, socolPermissionIndexkeeps behaving as ifLibraryIddoesn't exist (silently blank, not an error) until this is done. Studio → Data panel → find PermissionIndex → Refresh (or remove and re-add if there's no direct refresh option) → reload the app soApp.OnStartre-runs.
Launch(varTenantHost & "/teams/1598/_layouts/15/user.aspx?obj=" & EncodeUrl("{" & ThisItem.LibraryId & "}") & ",doclib&List=" & EncodeUrl("{" & ThisItem.LibraryId & "}"))
Select action silently dropping a field nothing downstream could see; First(GroupedItems).Field failing to surface a newly-added field even once genuinely fixed upstream (switched to a direct LookUp instead); and a URL needing the real site path plus proper percent-encoding, not just the tenant root and a literal {guid}.
2f. New Flow: FP - Rebuild Library Stats
Why: scrManageLibraries is meant to be a housekeeping/audit view — Allan's own framing: spot libraries created a long time ago with zero files in them, so nobody's actually using them, and flag them for a "are you going to use this?" conversation. Needs two new facts about each real library that nothing currently tracks: when it was actually created, and how many items are in it right now.
RebuildLibraryStats_flow_import.zip — import this like every other flow (My flows → Import → Import Package (Legacy)). Confirmed working, per Allan, 26 Sep 2026 — this one was hand-authored rather than exported from a real flow, and took two real structural bugs to get right (a missing second manifest.json, and a stray metadata block copied from the wrong kind of reference). Documented process and a real structural checker for this now exist (power-automate/power-automate-bible.md "Hand-Authoring A New Import-Package", PowerApps/tools/validate-flow-package.py) — use them first next time, don't hand-build from memory.
What it does (or the manual build steps below if a future re-import ever needs one):
- Create a new flow. Power Automate → My flows → New flow → Instant cloud flow → name it
FP - Rebuild Library Stats→ trigger: Power Apps (V2). - Trigger input (optional, not required):
SiteUrl, type Text — same optional override pattern every other flow here uses. - Get_Libraries — HTTP request to SharePoint (or "Send an HTTP request to SharePoint" action): GET, Site Address =
coalesce(triggerBody()?['SiteUrl'], 'https://modgovuk.sharepoint.com/teams/1598'), Uri:_api/web/lists?$filter=BaseTemplate eq 101 and Hidden eq false&$select=Title,Created,ItemCount,RootFolder/ServerRelativeUrl&$expand=RootFolder&$top=5000
&$top=5000is included from the start this time — the missing-libraries bug fixed in section 2b happened because an earlier flow forgot this on a similar query. - Select_LibraryStats — a Select action, From:
body('Get_Libraries')?['d']?['results'], Map:All three fields explicitly mapped — the same class of bug fixed in section 2e happened because a Select action silently dropped a field nothing further downstream could then see. Don't repeat that here.Url: item()?['RootFolder']?['ServerRelativeUrl'] Created: item()?['Created'] ItemCount: item()?['ItemCount']
- Apply_to_each_Library, foreach
body('Select_LibraryStats'):- Get_Existing_Row — HTTP GET, same Site Address as above:
_api/web/lists/getbytitle('LibraryFilePlan')/items?$filter=LibraryUrl eq '@{items('Apply_to_each_Library')?['Url']}'&$select=Id&$top=1— same dedupe-lookup pattern Backfill/Register-Existing-Library already use. - If_Tracked:
length(body('Get_Existing_Row')?['d']?['results'])greater than0. - Inside "Yes": Update_LibraryFilePlan_Row — HTTP POST, Uri
_api/web/lists/getbytitle('LibraryFilePlan')/items(@{body('Get_Existing_Row')?['d']?['results'][0]?['Id']}), HeadersAccept: application/json;odata=verbose,Content-Type: application/json;odata=verbose,IF-MATCH: *,X-HTTP-Method: MERGE(standard SharePoint REST update syntax, not specific to this app), Body:{ "__metadata": { "type": "SP.Data.LibraryFilePlanListItem" }, "LibraryCreated": "@{items('Apply_to_each_Library')?['Created']}", "LibraryItemCount": @{items('Apply_to_each_Library')?['ItemCount']} } - Nothing needed in "No" — a library that isn't tracked in
LibraryFilePlanyet is a Backfill problem, not this flow's job.
- Get_Existing_Row — HTTP GET, same Site Address as above:
- No
Responseaction needed — same as FP - Rebuild Permission Index, this is a fire-and-forget scan the app just reloads its own data after, it doesn't need anything handed back. - Save. Add it as a data source in Power Apps Studio (Data panel → Add data → Power Automate flows). Check the exact Power Fx identifier Studio gives it in the Insert/Data panel before wiring the button — don't assume it matches the display name exactly, the same lesson already learned twice this session (Delete Libraries, and the still-unconfirmed Create Library identifier).
OnSelect on scrManageLibraries uses RebuildLibraryStats as the data-source identifier — confirmed correct, working live, per Allan 26 Sep 2026.2g. Flow Surgery: Multi-Site Support (CCF Sub-Site)
{"actions": {}}. There is no code path anywhere in that flow that can touch an existing row's Team, Sub-Team, or PrettyName. The site loop only widens what gets discovered, never what gets overwritten.3. Download & Import Flows
CreateLibraryApp_flow_import.zip — creates a library, registers it into LibraryFilePlan. The core flow everything else builds on.
BackfillFilePlan_flow_import.zip — one-time scan of every existing library on the site, registers anything not already tracked.
RebuildPermissionIndex_flow_import.zip — scans libraries with broken permissions and builds a fast, searchable index of their groups.
ManageLibraryPermissions_flow_import.zip — lists, adds, or removes a group directly on a chosen library, called live from the Admin screen.
DeleteLibrariesApp_flow_import.zip — checks each library for files, deletes the ones that are safe to delete (or all of them if overridden), and cleans up their LibraryFilePlan/PermissionIndex rows. Called live from the Manage Libraries screen.
RebuildLibraryStats_flow_import.zip — scans every library's real creation date and file count, writes them onto the matching LibraryFilePlan row. Powers the housekeeping/audit view on Manage Libraries (empty old libraries flagged for review). New 26 Sep 2026 — see the callout in section 2f before trusting this one blindly.
All six: Power Automate → My flows → Import → Import Package (Legacy).
What Create Library (App) does
Triggered from Power Apps (or manually, for testing). Per library:
- Creates the library (
BaseTemplate 101) with the correct sensitivity label for Normal vs Limited - Adds a CAT choice column (fill-in-your-own enabled) and a Status choice column (Draft / Under Review / Reviewed / Published / Final / Superseded / Expired)
- Creates WiP view (items without a retention label) and Records view (items with one), both grouped by CAT
- Renames the library's Title to the pretty display name
- If Limited: breaks role inheritance completely, grants SG_Cadets-Admin-Ltd_MEM and SG_IHUB Full Control (fixed — nothing to fill in on the form, since a separate team owns creating/adding any other group later), then removes the flow-runner's own account (SharePoint auto-adds whoever breaks inheritance as a lockout safety net; the flow strips that back out)
- Registers the new library into
LibraryFilePlan
Site URL defaults to the main site if left blank — every action falls back to it via coalesce().
Testing Create Library (App)
Power Automate → My flows → Create Library (App) → Test → Manually.
| Field | First test — plain | Second test — Limited |
|---|---|---|
| Site URL | leave blank | leave blank |
| Technical Name | ZZTest_Library | ZZTest_LTD_Library |
| Display Name | ZZ Test Library | ZZ Test LTD Library |
| Is Limited | false / off | true / on |
| Team | Test | Test |
| SubTeam, Categories | leave blank | leave blank |
First test: check the library exists with CAT + Status columns and WiP/Records views, its Title shows the pretty name, and a new row appeared in LibraryFilePlan with PrettyName populated.
SG_Cadets-Admin-Ltd_MEM and SG_IHUB are there — no inherited groups, no extra groups, no direct user entry for whoever ran the flow.LibraryFilePlan once both tests pass.Backfill Existing Libraries
LibraryFilePlan only tracks libraries created after this system existed. This flow seeds it from what already exists: scans every real document library (skipping SharePoint's own system libraries), and for anything not already tracked, adds a row with LibraryUrl, PrettyName, Title, SiteUrl filled in automatically, and IsLimited guessed from whether the library already has broken permission inheritance. Team, SubTeam are left blank — fill those in by hand afterward via SharePoint's grid/quick-edit view. Safe to run more than once — only adds what's missing.
Rebuild Permission Index
Power Apps can't read SharePoint permissions directly, and a search screen can't afford to live-query every library's permissions on every keystroke. This flow scans permissions once and writes a flat, fast-searchable copy into PermissionIndex; the group-search screen just filters that list.
Only scans libraries with broken/unique permissions (mostly your Limited libraries) — Normal libraries all inherit the exact same site-wide permissions, so indexing each individually would be pure noise. If a group is a native SharePoint Group, it also looks one level inside that group's own membership — this is how a nested DG_... AD group shows up in the index too, without needing Microsoft Graph.
PermissionIndex and rebuilds it from scratch every run — intentional, avoids stale/duplicate rows.Manage Library Permissions
This is the write counterpart to Rebuild Permission Index / Group Search, which are both read-only snapshots. This flow is called live, on demand, from the Admin screen — one flow, one trigger, an Action field ("List" / "Add" / "Remove") decides what it does:
- List — live role assignments for one library, straight from SharePoint (not the cached index), so it's always current
- Add — grants a SharePoint group (
SG_...) a permission level on that library, reusing the exact sameGet_Group_Id → Get_Role_Def_Id → Add_Role_Assignmentpattern already proven in Create Library (App) - Remove — removes a group's entire access to that library in one call (
roleassignments/getbyprincipalid(id)with an HTTP DELETE), rather than needing to know which specific permission level to unwind
SG_...) directly on a library — matches how every Limited library in this system is actually set up: a DG_... AD group is never granted directly, it's always nested inside an SG_... wrapper group. If a genuinely new AD group needs adding, the other team creates it and its SG_ wrapper first, same as always — this screen doesn't change that workflow, just removes the "someone has to do it by hand in native SharePoint" step.Delete Libraries (App)
Called from the Manage Libraries screen with a JSON array of library URLs, an Action (currently always "Delete" from the app — "Check" is accepted too but every branch other than Delete just reports counts and changes nothing, so it's there if you ever want a preview-only call), and a Force flag.
Per library: gets its live ItemCount via _api/web/GetList(url) (server-relative URL, not title — the one join key in this whole system guaranteed not to drift if someone renames a library directly in SharePoint). If Force is false and ItemCount > 0, that library is skipped and reported back, untouched. Otherwise it deletes the document library itself, its LibraryFilePlan row, and every PermissionIndex row that pointed at it.
4. Building the Power App
Create a new blank canvas app in Power Apps Studio.
Data connections
Data panel → Add data → add these before pasting any screen YAML:
'LibraryFilePlan' (SharePoint → your main site → LibraryFilePlan) 'PermissionIndex' (SharePoint → your main site → PermissionIndex) 'Create Library (App)' (search Power Automate flows in Add data) 'Rebuild Permission Index' (search Power Automate flows in Add data) 'Manage Library Permissions' (search Power Automate flows in Add data) 'Delete Libraries (App)' (search Power Automate flows in Add data)
App.OnStart
colGSTeamGroups, colGSSubTeamGroups, colGSExpandedTeams, colGSExpandedSubTeams) that scrGroupSearch v3.14+ depends on. Full code is item 7 in the Everything Left To Do checklist near the top of this guide if it ever needs re-pasting from scratch.5. Screens
For each: delete the existing screen if you already have one pasted, New screen (blank) → open the link → Copy YAML → paste. Rename the screen to match the name shown.
Read-only browse screen — search box, Team → SubTeam → Library collapsible accordion, Open and icon-only Copy buttons per library, small padlock icon + red/salmon row tint on Limited libraries. v2.12: adds an admin-only Refresh button next to the search box (re-pulls straight from LibraryFilePlan — this screen otherwise only refreshes at app launch), and a Manage Libraries button on the nav bar, which was missing from this screen only. Full delete-and-repaste this time — the previous add-on-only paste attempt didn't take, so don't try pasting just the button again, replace the whole screen.
Restricted to the admin email list. Fill in a library's details, Add to Queue as many times as needed, then Run Queue creates them all sequentially, with a progress overlay. New toggle lets you register an existing library instead of creating one. Also has a Rebuild Permission Index button with a staleness indicator. Technical Name and Display Name are both auto-prefixed with the selected Team's own number (e.g. Team 04 → 04_SOP), plus a new optional "Extra prefix" field inserted between the two (e.g. 04_03 typed there gives 04_04_03_SOP) — Allan's own naming convention. v1.13 fixes real bugs found testing v1.12: Display Name wasn't getting prefixed (only the URL was), the Team dropdown and text fields were nearly invisible against the page background, the Sub-Team label wrapped and overlapped the field below it, and the queue list didn't show the actual computed result.
Search by SG_/DG_ group name or library name. Team → Sub-Team → Library → Group → Member accordion (Team/Sub-Team grouping added in v3.14, matching scrFilePlan). Reads the precomputed PermissionIndex list — instant, no live SharePoint crawl. A "View Permissions" button on both the library header and each Group row (v3.15), opening the library's real site (ThisItem.SiteUrl), CCF included. Red padlock = genuinely Limited (flagged or named _LTD/(LTD)); amber pencil = permissions customised but not marked Limited (v3.15, replaces the old purple-warning icon). v3.16: fixes the Team/Sub-Team/Library row hierarchy so size and indent actually step down with depth (15/13/12pt), and the Library row's count badge + chevron now indent together with its title when nested under a Sub-Team.
Restricted to admins. Tick libraries, bulk-delete them (empty ones go straight away, non-empty ones get an override prompt) or send the selection to the edit cart. Team → Sub-Team → Library accordion, Team filter dropdown, "Sub-Team: None" shown explicitly. Every library row shows its creation date and current file count, empty libraries flagged in red, plus a Refresh Library Stats button. Delete Selected/Leave Them/Delete Anyway show a proper Notify() banner on success (v2.7). v2.10: an Email CSV (current view) button attaches the libraries on screen as a CSV to an email to you. v2.9: file counts, age filters and Reload saved stats. v2.8: the Limited badge is now three honest states — red padlock (genuinely Limited), amber pencil (permissions customised, not marked Limited), red triangle (flagged/named Limited but permissions are still fully inherited — a real inconsistency worth checking).
Restricted to admins. The edit cart — one stacked form per library sent over from Manage Libraries, change whatever's needed on each (including Limited access, now editable here), Save All applies every change in one pass. v2.3 fixed the Team dropdown showing blank on a card that genuinely has a Team set (confirmed live). v2.4 carries the same sort-order fix as scrFilePlan v2.11.
v2.4 — 26 Sep 2026 — confirmed liveDesigned, never built
This one still doesn't exist in Studio, no current plan to build it. The YAML below is a design snapshot from August 2026, never confirmed live.
ModernCheckbox@1.0.2 using Default for its state (Allan's own swap after enabling Modern controls tenant-side). Every other control on this exact pattern in this app family has turned out to read Default only once, at creation, not reactively — confirmed elsewhere as the cause of a checklist that visually never ticked despite correct underlying data. If "Select All Visible" updates the data but doesn't visually check the boxes, that's this exact bug; the confirmed fix elsewhere was a control with a genuinely reactive property (Checked) instead.ForAll + Patch). Library URL is deliberately not editable — it's the join key this whole app relies on to survive renames. Includes a "Limited access" tick box per card (fixes the record only, not real SharePoint permissions if those are genuinely mismatched) and a working Team dropdown, pulling the real matching Choice record rather than a hand-built one.ThisItem.Title (the library's real current SharePoint Title) when calling the flow, not PrettyName/EffectiveName — SharePoint's getbytitle() needs the actual current Title, and Create Library (App) renames every library's Title to the pretty name as its last real step, so by the time a library shows up here, its Title already is the pretty name.7. Future Idea (Scoping Only, Not Started): Limited Sub-Site Migration
The problem: a separate sub-site (called "Limited") holds a large number of document libraries, every one of them genuinely Limited (its own permission groups, its own category setup), all created before this app's current build conventions existed. Microsoft is pushing tenants toward a flat site structure, so these need to move up to the main site (/teams/1598), not just sit on a sub-site indefinitely.
Hard constraint: no Microsoft Graph access. Allan has zero access to Graph, so nothing in this project can rely on it — not Graph's site-content-move-job APIs, not Graph-based file operations, nothing. Every flow in this app already only ever uses the SharePoint REST connector (_api/web/... via shared_sharepointonline) and that's the only mechanism available here too. This doesn't block the plan below — the one genuinely Graph-adjacent piece (bulk cross-site content transfer) is handled by SharePoint's own native browser UI, driven by Allan directly in his own session, not by an API call needing Graph permissions.
Decided direction: copy, not move. Allan's explicit call — leave every original library fully in place and untouched, copy its documents to a newly-created library on the main site, verify the copy is complete and correct, and only then flag (never auto-delete) the original for later manual deletion.
Allan's own curiosity, answered: do links break?
A SharePoint file's URL (and its internal unique ID) belongs to that specific file object, not to "the document" in some abstract sense. Since a copy leaves the original completely untouched, any existing link to the original — a "Copy link" share, a bookmarked URL, a link pasted in an email months ago — keeps working exactly as it does today, for as long as the original still exists. A link to the new copy is a brand-new URL; nothing carries old links over to it automatically, so anyone needing to reach the new copy needs a freshly generated link. The only point anything actually breaks is if/when the original is eventually deleted for real (whenever that happens, on Allan's own timeline, likely well after this) — at that point any old link still pointing at it will 404, same as deleting any file would do regardless of how it got migrated. Worth a quick real test (copy one file, confirm the original's existing link still resolves) before relying on this for anything that matters, rather than trusting this write-up alone.
What the new library needs, beyond a raw structural clone
- Full modern treatment, not a copy of the old setup. Built via the same logic as Create Library (App) — CAT column (fill-in-your-own), Status column, WiP/Records views, and critically the correct sensitivity label for Limited libraries, which a lot of the old sub-site libraries currently lack entirely. This is explicitly an upgrade on migration, not a like-for-like clone.
- Category values, not just the column. The old library's CAT column has real values already in use across its documents — these need identifying (distinct values actually used, not a guess) and pre-seeding onto the new column's choices, and each copied document needs its CAT value re-applied to match on the new side so nothing loses its category during the move.
- Same display name, but conformed to current naming (numeric team prefixes, matching every other library built since). Team/Sub-Team assignment per migrated library is a human decision Allan makes at migration time, not something to infer automatically from the old sub-site's own structure.
- Permissions replayed exactly, not recreated from scratch. Every source library's real grants already exist in
PermissionIndex(GroupName/PermissionLevelper library) once a rebuild's been run against the sub-site too. The underlying SG_/DG_ groups already exist tenant-wide, so this is just re-applying existing group references at the same permission levels on the new library — no new groups need creating, matching Allan's own read of it ("permissions are not going to change... they exist already").
Verification before flagging anything for deletion
Given every one of these libraries is genuinely Limited, getting this wrong is a real security question, not just a UX one. Two checks, both cheap to build and both non-negotiable before a library gets flagged:
- Permissions diff — re-scan the new library's real live permissions (same mechanism
FP - Rebuild Permission Indexalready uses) and compare against what was intended to be replayed from the source. Any mismatch blocks flagging, full stop. - Content diff — confirm every document that existed on the source made it to the new library (file count and name comparison at minimum) before treating the copy as complete.
Only once both pass: cosmetically rename the old library's Title (its URL stays untouched, so nothing that already points at it breaks) to visibly flag it — something like appending "(Migrated — pending deletion)" — and add a matching field to LibraryFilePlan so it surfaces on scrManageLibraries with its own tick-box. Not every flagged library will necessarily get deleted on the same timeline, or at all — some may just get left, so this needs to be an explicit per-library opt-in Allan controls, never an all-or-nothing switch.
Open questions, not yet decided
- Separate small standalone tool, or a new screen bolted onto this app? Allan floated both as genuinely live options — this is a largely one-off bulk migration job, not a recurring day-to-day feature, so a cheap bespoke tool with just a handful of buttons might suit it better than extending the main app. Worth deciding at the start of the actual work, not guessing now.
- Exact automation boundary for the document copy step itself — whether it's driven entirely by Allan using SharePoint's native "Move to"/"Copy to" UI per library (simplest, safest, no new flow risk) or whether enough volume exists to justify scripting bulk copies via the SharePoint REST connector instead. Given "loads of them," worth a real count before assuming either way.
Recommended first step, whenever this actually starts (safe, read-only, useful on its own)
A pure audit/discovery pass before touching anything: scan every library on the Limited sub-site and produce a CSV (or a page in this guide) listing each one's name, its real current permission grants, the CAT values actually in use, document/version counts, and whether it already has a proper sensitivity label or not. Same read-only-scan shape as Rebuild Permission Index/Backfill File Plan already proven in this app — genuinely low-risk, and gives Allan and Claude something concrete to review together before any migration logic gets designed in detail.
Issues
Found a problem? Report it here.