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.
App.OnStart (v3.12)— done, per Allan, 25 Sep 2026.Paste the 3 updated screens— done: scrFilePlan v2.9, scrAdminCreateLibrary v1.9, scrGroupSearch v3.8 all pasted successfully.Paste scrManageLibraries— pasted, v2.1 had two real bugs (wrong flow name + a syntax error), both fixed in v2.2. A third error remains, but it's not a code bug: FP-DeleteLibraries(App) has never actually been run, so Power Apps has no sample response to learn its shape from — every field insideResultsshows as type "Error" until it has. Fix: Power Automate → FP-DeleteLibraries(App) → Test → Manually, run it once withAction="Check"(this mode never deletes anything, entirely safe) and any real library URL inLibraryUrlsJson. Then refresh the connection in the Power Apps Data panel and re-check.scrEditLibraries errored on v2.0— fixed in v2.1, ready to retry: scrEditLibraries v2.1. Two real control-property bugs, explained in the Screens section below. Delete any partially-pasted screen first, then paste fresh.- Confirmed 25 Sep 2026: the "Delete Libraries" flow's current name is
FP-DeleteLibraries(App)(Allan's own rename). It didn't exist in Power Automate at all until imported from DeleteLibrariesApp_flow_import.zip — if not done yet, import it (My flows → Import → Import Package (Legacy)) and add it as a data source before pasting scrManageLibraries. - Once scrManageLibraries v2.2 and scrEditLibraries v2.1 are both in: test the actual intended flow — tick a library's checkbox on Manage Libraries, click Edit Selected, confirm you land on populated cards (not empty ones). Report back if that specific path doesn't work.
- Optional, whenever you want that specific feature: Register Existing Library flow surgery (2d) for the new toggle on Create Library, and/or the Backfill name fix (2c) if you plan to run Backfill again.
- Once anything above is pasted and confirmed live, tell me — I'll re-pull the exact flow names (they'll have changed since your rename) and update this box.
- 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 |
_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 |
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)
Cause: found 25 Sep 2026 while investigating why some libraries showed raw, ugly names in scrFilePlan instead of a proper Pretty Name. This flow's own Create_FilePlan_Row action was writing to DisplayName, not PrettyName — the exact same silently-dropped-write bug fixed in section 2 above for Create Library (App), but never applied here since the two flows only share a pattern, not the actual action. Any library that was ever registered by running Backfill (rather than created through the app's own Create Library button) has no Pretty Name at all as a result.
In Power Automate, open Backfill File Plan → Edit → find the Create_FilePlan_Row action inside the If_Not_Already_Tracked condition, inside Apply_to_each_Library → click into its Body box → select all, delete → paste this in as the replacement → Save.
{
"__metadata": { "type": "SP.Data.LibraryFilePlanListItem" },
"Title": "@{items('Apply_to_each_Library')?['Title']}",
"LibraryUrl": "@{items('Apply_to_each_Library')?['Url']}",
"PrettyName": "@{items('Apply_to_each_Library')?['Title']}",
"SiteUrl": "@{coalesce(triggerBody()?['SiteUrl'], 'https://modgovuk.sharepoint.com/teams/1598')}",
"Team": "",
"SubTeam": "",
"IsLimited": @{if(equals(items('Apply_to_each_Library')?['Unique'], true), 'true', 'false')}
}
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 (reload, or hit any button that re-pulls it).2d. Flow Surgery: Create Library (App) — Register Existing Library
Needed for scrAdminCreateLibrary v1.9's new "Register an existing library only" toggle. This adds a second path through the same flow — nothing gets deleted or recreated, and no new flow needs registering as a data source in the app.
- Add two new trigger inputs. Open Create Library (App) → Edit → click the trigger (Power Apps) → under its inputs, add:
RegisterOnly(type Yes/No, not required) andExistingLibraryTitle(type Text, not required). - Add two "Initialize variable" actions right after the trigger, before
Select_Categories:varOutDisplayName(String, leave the value blank) andvarOutLibraryUrl(String, leave the value blank). - Add a Condition action right after those two, still before
Select_Categories. Set it to checkRegisterOnly is equal to true. - Move the existing chain into the "No" branch. Select
Select_CategoriesthroughCreate_FilePlan_Row(everything except the final Response action) — multi-select them (click the first, shift-click the last, or use the "..." menu → Select actions), Cut, then click inside the Condition's No branch and Paste. - At the end of the "No" branch (after
Create_FilePlan_Row), add two Set variable actions:Set varOutDisplayNametotriggerBody()?['DisplayName']Set varOutLibraryUrltobody('Get_New_Library_Url')?['d']?['RootFolder']?['ServerRelativeUrl']
- In the "Yes" branch, add these actions in order:
- An HTTP action (Send an HTTP request to SharePoint) named
Get_Existing_Library_Url: GET_api/web/lists/getbytitle('@{triggerBody()?['ExistingLibraryTitle']}')/RootFolder/ServerRelativeUrl, Site Address = the same dataset expression already used elsewhere in this flow (coalesce(triggerBody()?['SiteUrl'], 'https://modgovuk.sharepoint.com/teams/1598')). - Another HTTP GET,
Get_Existing_Row:_api/web/lists/getbytitle('LibraryFilePlan')/items?$filter=LibraryUrl eq '@{body('Get_Existing_Library_Url')?['d']?['ServerRelativeUrl']}'&$select=Id&$top=1— same dedupe check Backfill already uses. - A Condition,
If_Not_Already_Tracked:length(body('Get_Existing_Row')?['d']?['results'])is equal to0. - Inside its "Yes": an HTTP POST,
Create_FilePlan_Row_Register, same URI as the existingCreate_FilePlan_Row(_api/web/lists/getbytitle('LibraryFilePlan')/items), with this body:{ "__metadata": { "type": "SP.Data.LibraryFilePlanListItem" }, "Title": "@{triggerBody()?['ExistingLibraryTitle']}", "LibraryUrl": "@{body('Get_Existing_Library_Url')?['d']?['ServerRelativeUrl']}", "PrettyName": "@{coalesce(triggerBody()?['DisplayName'], triggerBody()?['ExistingLibraryTitle'])}", "Team": "@{triggerBody()?['Team']}", "SubTeam": "@{triggerBody()?['SubTeam']}", "SiteUrl": "@{coalesce(triggerBody()?['SiteUrl'], 'https://modgovuk.sharepoint.com/teams/1598')}", "IsLimited": @{if(equals(triggerBody()?['IsLimited'], true), 'true', 'false')} } - Still inside the "Yes" branch of
If_Not_Already_Tracked, after that action:Set varOutDisplayNametotriggerBody()?['ExistingLibraryTitle'], andSet varOutLibraryUrltobody('Get_Existing_Library_Url')?['d']?['ServerRelativeUrl']. - In the "No" branch of
If_Not_Already_Tracked(already registered): the same two Set variable actions, so the flow still responds cleanly instead of leaving the variables blank.
- An HTTP action (Send an HTTP request to SharePoint) named
- Move the existing
Responseaction out from directly afterCreate_FilePlan_Rowto sit after the whole Condition block (both branches). Update its body to read from the variables instead of the old direct references:{ "Success": true, "DisplayName": "@{variables('varOutDisplayName')}", "LibraryUrl": "@{variables('varOutLibraryUrl')}" } - Save. Test both paths manually before touching Power Apps: once with
RegisterOnlyoff (normal creation, unchanged), once with it on and a real existing library's title.
Terminate action in the "Yes" branch: a flow that never reaches its own Response action can surface as an error on the Power Apps side. Routing both paths through one shared Response, fed by variables set in whichever branch actually ran, is the same fix pattern already proven elsewhere in this app family for "flow needs one shared response after branching logic."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.
All five: 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
Select the App object (top of the tree, not any screen) → OnStart property → paste this in full:
colPermissionIndex at the single load point so every screen that reads it (currently just scrGroupSearch) automatically excludes "Limited Access" rows (SharePoint's own auto-generated per-item grant noise) and any individual "User" rows — this is meant to be a group-only view. v3.10 gives any library with a blank Team an "Uncategorised" label instead of grouping under a blank/invisible header, and force-sorts that group to the bottom regardless of alphabetical order — needs the matching scrFilePlan v2.7 and scrAdminCreateLibrary v1.7 pasted too, since the same grouping logic is rebuilt in both those screens. v3.11 adds a genuine third grouping tier using the existing (until now unused-for-grouping) SubTeam field: Team → SubTeam (only when set) → Library, same flat-gallery pattern scrGroupSearch already proves. Also splits the sort key away from the display name — sorting is now on the real Title, not PrettyName/EffectiveName, so giving a library a nicer display name never silently reorders the list. v3.12 adds two new columns to colQueue's seed table — RegisterOnly and ExistingLibraryTitle — for the new "register an existing library" path on scrAdminCreateLibrary. Needs the matching Create Library flow change (see the Flows section) before Run Queue will work correctly for that path.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 = "", "", 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);
ClearCollect(colPermissionIndex, Filter('PermissionIndex', PermissionLevel <> "Limited Access" && PrincipalType <> "User"));
ClearCollect(colPermissionFiltered, colPermissionIndex);
ClearCollect(colLibraryGroups, Sort(AddColumns(GroupBy(colPermissionFiltered, LibraryUrl, LibraryTitle, GroupedItems), ItemCount, CountRows(GroupedItems)), LibraryTitle, SortOrder.Ascending));
Clear(colPermFlatSource);
Collect(colPermFlatSource, AddColumns(colLibraryGroups, RowType, "Library", SortKey, 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", SortKey, 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", SortKey, LibraryUrl, RankInGroup, 2, GroupSortKey, NestedUnderGroup));
ClearCollect(colExpandedLibs, Filter(Table({LibraryUrl: "seed"}), false));
ClearCollect(colExpandedGroups, Filter(Table({GroupKey: "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 — self-heals every library missing a pretty name, no data migration needed. varIsAdmin gates the Admin screen — edit the email list directly in this line if who's authorized ever changes. colPermissionIndex/colPermissionFiltered feed the Group Search screen — make sure you've run Rebuild Permission Index at least once first, or this loads empty.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.9 — 25 Sep 2026 — draft, not yet pastedRestricted 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.
v1.9 — 25 Sep 2026 — draft, not yet pastedSearch by SG_/DG_ group name or library name. Results grouped by library in a collapsible accordion, same pattern as scrFilePlan. Reads the precomputed PermissionIndex list — instant, no live SharePoint crawl.
v3.8 — 25 Sep 2026 — draft, not yet pastedReady to build
These two are newly built from scratch (25 Sep 2026), rebuilt from an old August design that was never actually pasted into Studio. Paste as a brand new blank screen — there's nothing existing to delete first. Nav bar is already included in the paste, no separate add-on step needed.
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.
v2.2 — 25 Sep 2026 — v2.1 errored on paste, see belowRestricted 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.1 — 25 Sep 2026 — new, not yet pasted (v2.0 errored on paste, see below)Designed, 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.
.Run() call has no true non-blocking mode (a slow response is caught and shown as a friendly message rather than a raw error, but the flow itself always runs to completion on its own regardless). The status line above it shows how long ago the index was last rebuilt, and flags how many Limited libraries have been created since — both computed live from data already loaded, no extra flow or list needed. v1.7 (draft): after Run Queue finishes, the refreshed file-plan collections now use the same "Uncategorised" grouping as scrFilePlan v2.7, so a newly-created library with no Team assigned doesn't fall back to a blank grouping until the next full app reload. v1.8: same update again for the SubTeam grouping tier added in scrFilePlan v2.8, for the same reason. v1.9 adds a "Register an existing library only" toggle (default off — unticking swaps Technical Name for an Existing Library Title field, hides Categories since no columns get created on this path), a "Manage Libraries" nav button, and disables the dead "Manage Permissions" nav button this screen's own copy had never had fixed. Needs the matching Create Library flow change described in the Flows section below — without it, ticking the box adds a queue entry but Run Queue won't actually register anything.RowType and a second expand-tracking collection (colExpandedGroups) rather than nested galleries. v3.1 fixed a real sort-order bug where member rows sorted after every group in a library instead of clustering under their own parent group — the sort's column priority had rank ahead of the group-clustering key instead of after it. v3.2 moved the counts directly into each chevron instead of a separate pill: 4 > on a Library row (how many groups directly granted), (2) > on a Group row (how many nested members) — visible whether the row is expanded or collapsed. v3.4 split the count back out into its own label and replaced the text chevron with a proper vector icon; added hover/press feedback on both Library and Group rows (previously gave no visual response); added a "Copy link" button on Library rows to match scrFilePlan; and added a friendly message when a search matches nothing. v3.5 fixed the same two bugs found on scrFilePlan v2.4: opaque hover fills blacking out row text instead of tinting it, and real library titles with underscores wrapping mid-word and overflowing into the row below (now shown with spaces, row made taller). v3.6 is a draft, not yet pasted: carries over the same full-width layout and icon-button treatment Allan applied to scrFilePlan v2.6, and disables the same dead "Manage Permissions" nav button. v3.7 (also draft): Security Group rows now get a distinct amber tint and no chevron/expand affordance at all — they never have enumerable members in this data (only SharePoint Groups do), so a chevron implying they could be expanded was misleading. Needs the matching App.OnStart update (v3.9) pasted too, which filters out "Limited Access" noise rows and individual User rows at the source so every screen reading colPermissionIndex benefits automatically. v3.8 adds a "Manage Libraries" nav button now that screen actually exists.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.Force: false on the first call — any empty library in your selection is deleted immediately, any non-empty one is skipped and reported back in one round trip, no separate "check" step needed first. If anything was skipped, an override panel lists exactly which ones and their item counts; "Delete Anyway" re-sends only those, this time with Force: true. Uses CheckBox@0.0.30 (per-row tick, read/write via its Checked property) and ModernButton's BasePaletteColor for the red delete buttons — Modern buttons don't have a direct Fill property, unlike Classic buttons used everywhere else in this app; light seed colours get overridden by Fluent 2's contrast rules, but the dark red used here renders correctly. Full-width layout and the same "Uncategorised"-aware collection rebuild used everywhere else in this app, so a delete here doesn't leave scrFilePlan showing stale groupings until next reload. Needs the "Delete Libraries" flow added as a data source before pasting. v2.1: same padlock-icon + red/salmon row tint treatment as scrFilePlan v2.9, replacing this screen's own LTD badge which had drifted to a different amber tone — now consistent across the app. v2.1 errored on paste, real errors, both fixed in v2.2: the flow reference was still the pre-rename name, 'Delete Libraries (App)' — confirmed current name is 'FP-DeleteLibraries(App)', now used in both .Run() calls. Also a genuine syntax bug: one branch of an If() wrapped a chain of statements in an extra pair of parentheses, which Power Fx doesn't support (parens group one expression, not a statement sequence) — removed, matching the plain semicolon-chain-with-no-wrapping-parens pattern already proven working elsewhere in this app.colEditQueue), shown here as one stacked card per library, all editable at once. Every field edit patches straight into that local collection as you type (no flow, no round trip); "Save All" is the only point anything actually writes to SharePoint, via a single ForAll + Patch. Library URL is deliberately not editable here — it's the join key this entire app relies on to survive renames, so changing it needs to stay a rare, manual, direct-in-SharePoint action, not a button. New: a "Limited access" tick box per card, so a wrongly-flagged LTD badge can actually be corrected now — note this only fixes the record, not real SharePoint permissions if those are genuinely mismatched. v2.0 errored on paste, real PA2108 errors, both fixed in v2.1: the dropdown's default-selection property was `DefaultSelectedItems` (a Combo box property, wrong control) instead of `ModernDropdown`'s actual `Default`; and the Limited-access control was a `Toggle`, which doesn't support any change-event property at all in this environment (confirmed — every real Toggle usage in the wider guide library only ever reads `.Checked` on demand, never reacts to it) — swapped for a `CheckBox`, same proven pattern already live in scrManageLibraries' own row-select tick boxes.Issues
Found a problem? Report it here.