Skip to content

Add Tools with Copy site structure - #56

Open
Gabriel Mongefranco (gabrielmongefranco) wants to merge 46 commits into
mainfrom
feature/tools-copy-site
Open

Gabriel Mongefranco (gabrielmongefranco) wants to merge 46 commits into
mainfrom
feature/tools-copy-site

Conversation

@gabrielmongefranco

Copy link
Copy Markdown
Contributor

This pull request adds a Tools section to the report screen, with one tool, Copy site structure (#34). It copies a site's structure into another site where the person is a site collection administrator. It copies no list items and no library files, apart from site pages, Site Assets, and agents and site skills.

Merge order

This branch is built on top of #55, so its first six commits are the commits of #55.

What it copies

  • Lists and libraries you choose, each with a new name and address. The copy creates each list with its address first, then sets its name. It also copies columns, views with their formatting, column formatting, content types, and form settings.
  • Columns in a safe order. Plain columns come first, then lookups after the lists they read from, then managed metadata columns after their term sets. Calculated columns come after the columns they use, and the columns a lookup shows come last.
  • Site columns and site content types, including column display names set in a content type. One example is Enterprise Keywords shown as "Tags".
  • Term sets of the site's own term group only, never the organization's. If term sets can't be copied, the columns that need them are skipped, and the results name every skipped column and term.
  • Site pages, Site Assets, and agents and site skills, each with its own switch. Every link and ID that points at this site is changed to point at the destination. Links to other sites and external links stay the same.
  • Navigation: the left menu, the top menu on classic sites, and the footer, with links that point at this site changed to the destination.
  • Site look: theme, header, logo, and footer settings.
  • Permissions, only when that switch is turned on. It is off by default.
  • A data dictionary file can be the source instead of this site. Then pages, Site Assets, agents and site skills, navigation, site look, and permissions are grayed out and turned off.

Before and after the copy

  • Check destination refuses a site on another host and a site where the person isn't a site collection administrator.
  • Before you copy runs read-only checks and lists what will fail, be skipped, or may fail before anything changes. Examples include term store rights, columns that need a term set you didn't choose, custom scripts turned off, and a subsite that shares its groups with its parent. Copy anyway and Go back replace the browser's confirmation box. A problem that would stop the copy offers only Go back.
  • After the copy, Sitenalyzer reads the destination back and corrects any result it can't confirm. Save results (.xlsx) saves the results table.
  • The copy never removes what the destination already had. This covers access and group members, a list's content types and New menu, a destination's own file, and an existing column's settings. The few changes it does make to existing items, such as replacing a menu it has read first, are named in the results.

Code layout

The new code is in src/tools/. build.sh and build.ps1 join those files into sitenalyzer.js in a fixed order, so the app is still one script with no new globals. The last commit adds the rebuilt dist/, so you can upload it to a test site.

Documentation is in the new docs/how-to/copy-a-site.md, plus updates to docs/usage.md, docs/architecture.md, docs/design-decisions.md, docs/troubleshooting.md, docs/data-dictionary.md, docs/development.md, and the README.

Testing

  • bash build.sh and pwsh build.ps1 write identical files.
  • Every commit in this branch builds.
  • The full Playwright suite passes: 481 tests.
  • The new tests run against a pretend SharePoint in tests/pretend-*.js. It answers the REST and CSOM requests the copy sends, and it records every change so tests can check the destination afterwards.
  • Nothing in this pull request was tested on a real SharePoint Online site.

Checks to do by hand on a real site

  1. Rights. Check that copying into a site where you are not a site collection administrator is refused.
  2. Lists and columns. Copy a site with a lookup, a column that shows another column of a lookup, a managed metadata column, and a calculated column. Check each in the destination.
  3. Term sets. Copy into a site with no term group of its own, then into one that has one. Compare Before you copy with what happened.
  4. Tags. Check that a content type column renamed in this site, such as Enterprise Keywords shown as "Tags", has the same name in the destination.
  5. Pages. Open copied pages. Check that links to this site open the destination, and that external links are unchanged.
  6. Navigation. Check menu order, hidden links, audiences, and the footer. On a communication site, check whether its top menu is copied or reported.
  7. Site look. Check the theme, logo, and header. On a site with a Microsoft 365 group, check the site logo thumbnail.
  8. Permissions on a top site. Turn on Permissions and copy into a top site. Check that new lists keep only the access they have in this site.
  9. Permissions on a subsite. Copy into a subsite that uses its parent's permissions. Check that no one is added to the parent's groups.
  10. A second copy. Copy into the same destination again. Check that nothing is duplicated and nothing the destination had is removed.
  11. Content Security Policy. Open the page with ?csp=enforce and check the browser console for Content Security Policy messages.
  12. Keyboard and screen reader. Go from the destination to the results with the keyboard alone, and listen to the status messages with a screen reader.

Closes #34

When both copy methods fail, the status message now says to select the
ID shown and press Ctrl+C (Cmd+C on a Mac) instead of asking the person
to copy it manually. The clipboard tests and the usage page now expect
and describe the new message. Nothing else about copying changes.

Closes #52
Both Refresh sites list controls used to remove the saved sites and empty
the list before asking SharePoint search again, so when both the sorted
request and the unsorted retry failed, the list and its saved copy were
gone and Choose a site said no sites had been found yet. The loader now
keeps the sites from the last successful load in memory and in browser
storage until a new answer arrives, replaces them only when SharePoint
answers (including with an empty list), and resolves to a loaded or
kept outcome. The helper text under the Site address box is now a live
status region, and it and the Choose a site status line say that the
list couldn't be refreshed or loaded and what to do next. The typed
address, the Find a site text, the open dialog, and focus are left as
they were, and a second refresh while one is running shares the request.

Playwright tests cover a failed refresh at both controls, a first-ever
failure, a retry that works, an empty answer, and blocked browser
storage. The usage, troubleshooting, and development pages describe the
behavior.
Sitenalyzer built links to site pages and shared items with encodeURI,
and built top-level folder links from the raw server-relative path, so a
name with a # became a fragment and a name with a literal % or %23 was
read as an escape. A new helper, snaEncodeServerRelativePath, encodes
each folder and file name of a path SharePoint returns as typed, such as
FileRef and ServerRelativeUrl, and keeps the slashes between them. The
page inventory, the shared file and folder lookup, the top-level folder
links, and the agent and site skill rows all use it now. Folder links are
also kept only when they are inside the site being read, as page and
shared item links already were. Addresses SharePoint has already formed,
such as DefaultViewUrl, are left as they are.

Site pages, the page details, and the print document now show each page's
address with the names as typed, and only the link uses the encoded form.
This keeps a value in a file name, such as an email address, hidden like
any other text while Show hidden values is off.

A browser test supplies pages, folders, a shared file, and a shared
folder whose names hold #, %, the text %23, spaces, an apostrophe, and
an accented letter, and checks the links on screen and in the print
document. The test list in docs/development.md describes it.

A reviewer should open a real site with a page and a folder whose names
contain # and %, and check that Open page and the folder link open the
right item in a new window with SharePoint's page router turned off.
Search this report now finds a list, library, column, or term by a word
that is only in its description, with the same quoted-phrase and wildcard
rules as names. Each search index entry keeps the saved description, and
the search hides values that look like personal information in it before
comparing, as it already does for names and synonyms, so a hidden phone
number cannot be found while Show hidden values is off. A description
saved in the editor is searchable at once because the index is rebuilt
after the save; a draft that was not saved is not indexed.

The search results help and the usage page now mention descriptions, and
the tests cover a keyword found only in a list, a column, and a term
description, quoted phrases and wildcards in descriptions, a hidden phone
number in a description, and the saved and unsaved description cases.
When a view was replaced, Sitenalyzer kept each table's filter text but
not its sort order, so a sorted table came back in its usual order after
opening another section or list tab, or after Show technical details
redrew the view. Each table's sort order is now remembered by its table
ID in memory, next to its filter text, and set again when the table is
rebuilt. A table that was never sorted opens in its usual order.

A remembered sort by a column the table can't show now, such as a
technical column while Show technical details is off, is left out when
the table is built, and comes back when the column is shown again.
Refresh report, Change site, and Start over clear the remembered order
with the rest of the report. No browser storage is used.

The new browser tests cover the restored order, sort indicator, and
filter text; another table staying in its usual order; keyboard sorting
after the order is restored; a sort by a technical column; and Refresh
report and Start over. Please check by hand with a screen reader that a
restored heading announces its sort order, and check the change on a
SharePoint page with ?csp=enforce.
The filter box above a table compared each row's address in its encoded
form. Since page, folder, and shared item links now encode every part of
the path, an @ in a file name became %40, so the check for hidden values
no longer saw an email address there. Typing part of that address into
the Site pages or Top-level folders filter box found the row while Show
hidden values was off.

The filter now decodes every address field before it hides values and
compares, the way agent and site skill addresses were already handled.
Links keep their encoded addresses.

A new test types part of a hidden email address into the Site pages and
Top-level folders filter boxes and checks that nothing matches until
Show hidden values is on. It fails without this change.

Refs #42
This adds a Tools section to the report screen, at #sna/tools, with one
part, Copy site structure, at #sna/tools/copy. It is the base that the
rest of issue #34 builds on: the screen, the copy plan, the order of the
copy steps, the check of the destination afterwards, and the results.

The screen has a source (this site's report, or a data dictionary file),
a destination address with Choose a site and Check destination, a table
of lists and libraries with a new name and address for each, the term
sets of the site's own term group, four switches (Site pages, Site
Assets, Agents and site skills, and Permissions, which is off by
default), and a Replace checkbox. The destination check refuses an
address on another host before sending any request, and refuses a site
where the person isn't a site collection administrator. Copy to
destination reads what the copy needs from this site, asks the person
to confirm, runs the steps, reads the destination back, and shows a
results table that Save results (.xlsx) saves. Stop copying, Start
over, Change site, and Refresh report stop a copy before its next
request.

The code is in src/tools/. core.js has the one helper that writes to a
site, plan.js reads full list settings, columns with SchemaXml, views,
content types, site columns, and term records, runner.js runs the steps,
verify.js checks the destination, and ui.js draws the screen. Creating
lists and libraries works now. Term sets, columns, views, content types,
files, pages, permissions, and reading a data dictionary file are not
built yet: their files hold the functions the steps call, and each item
they would copy shows as Skipped with a note.

In sitenalyzer.js, request digests are now kept for each site, because
SharePoint accepts a digest only on the site that issued it, and a write
asks for a new digest once when SharePoint refuses one. The existing
description saves and $batch reads use the same function as before.
Choose a site has a second mode that fills the destination box instead
of opening a report. The print document leaves Tools out.

The tests add a pretend destination site with an in-memory model, the
reads the copy plan makes from the pretend source site (without changing
any answer the existing tests use), and tests/tools.spec.js. The docs
describe src/tools/, the write helper, the per-site digests, and the
pretend destination, and the project terms add Tools, Copy site
structure, and Destination.

A reviewer must check by hand on a real tenant, with a destination site
that can be thrown away: that Check destination is right for a site
collection administrator and for someone who isn't one; that a copied
list gets the chosen address and then the chosen name, with the
destination's own digest; that every $select the copy plan sends is
accepted; and that the screen works with a screen reader, at 200% zoom,
and with ?csp=enforce and no Content Security Policy violations.
Copy site structure now copies the term sets the person chose from this
site collection's own term group into the destination site collection's
own term group. When the destination has no term group of its own, the
copy creates one through CSOM with TermStore.GetSiteCollectionGroup, using
TaxonomySession.GetTaxonomySession. It never creates or changes a
tenant-level group.

For each chosen term set, the copy uses a destination term set with the
same name when there is one, and otherwise creates it. It then creates the
terms with parents before children, with their synonyms, descriptions in
the destination's languages, and custom properties. A term under two
parents is created under the first and reused under the others. Deprecated
terms and terms that are unavailable for tagging get those settings last.
Terms already under the same parent with the same default label are used
instead of being created again, so a second copy adds nothing twice. Labels
in languages the destination's term store doesn't have are left out, with
one note for each term set.

Each change goes to the term store REST API (/_api/v2.1) first. When
SharePoint refuses it with HTTP 403 or a message about OAuth or
notAllowed, that change and every later one go through CSOM. When CSOM
fails too, the chosen term sets that aren't copied yet are reported as
failed and recorded as missing, so the columns step can skip the columns
that use them. Term sets the person left out are recorded as missing as
well. Every term that couldn't be copied, or was copied only in part, has
its own row in the results, with what happened and what to do next.

The module also adds three helpers for the columns step: one that connects
a managed metadata column to its term set through CSOM, one that finds the
destination's TaxonomyHiddenList, and one that checks that a tenant-level
term set can be found from the destination.

A maintainer must check these on a real tenant with a site collection
administrator who isn't a term store admin: that the CSOM requests with
GetTaxonomySession work and GetSiteCollectionGroup creates the group;
that the REST writes with browser cookies are accepted or refused in the
way the copy expects; which term a reuse relation's address names; that
the isOpen, isAvailableForTagging, and isDeprecated changes are accepted;
the form of parentSiteId in the group list; and that setting SspId,
TermSetId, and AnchorId on a column reached through Fields.GetById binds
it without a cast.
Copy site structure now copies each chosen list or library with its
settings, columns, site columns, content types, views, and form
settings. Columns are made in passes across every list so each one can
find what it uses: plain columns, then lookup columns (after every list
exists, pointed at the destination's list and site), then managed
metadata columns (after the term sets, with the hidden Note column
first, pointed at the destination's term store and TaxonomyHiddenList,
then connected to the term set and read back), then calculated columns
in the order their formulas use each other, then the columns a lookup
column shows, made with adddependentlookupfield. Column formatting, the
Title column, and the list's validation formula follow. The default view
is updated in place, other public views are made with views/add with
their layout and formatting, personal views are skipped, and hidden
views are counted in one row per list. Site content types keep their
source IDs, parents first, with their columns added through CSOM.

A chosen list or library whose address matches one the destination
already has with the same template, such as Documents, is no longer an
error. The address box says the copy adds what is missing, and the copy
uses that list: it adds the columns and views it doesn't have and leaves
the rest alone. A match with another template is still an error.

Failures are handled one item at a time. Each column, view, content
type, and form gets its own result with a note that says what happened
and what to do, and a stop of the copy is passed on. A managed metadata
column whose term set wasn't copied, or that the destination's term
store doesn't have, is skipped with a note naming the column, the list,
and the term set.

Check these by hand on a real tenant, because the pretend site only
models them: whether createfieldasxml keeps internal names with Options
8; which formula form SharePoint accepts for calculated columns; that
the CSOM requests for content type columns (FieldLinks.Add) and for
connecting managed metadata columns work as written; that views/add
accepts ViewType2, ViewData, and CustomFormatter and that MERGE on the
default view accepts ViewType2; and the UniqueContentTypeOrder body.
Copy site structure now copies the files it promises to copy. Site Assets
is copied folder by folder, with empty folders, in path order, leaving out
the library's Forms folder. The destination gets a Site Assets library
when it has none. Agents (.agent files) are copied to the same place in
the matching destination library, which is the library the copy made from
theirs or a library at the same address; each copied agent's note says it
still uses the original site's knowledge sources. Site skills are copied
folder by folder to Skills/<name>/ in the destination's Agent Assets
library, never the Plans folder. When Agent Assets is missing, the copy
turns on its feature and looks again, and skips every skill with a note
when it is still missing.

Modern pages are made again through the site pages API (create, save,
publish), with this site's address, list IDs, view IDs, file IDs, site
ID, and web ID in their content changed to the destination's. A page
that still holds an ID of this site with no match in the destination is
marked Partly copied. Pages in Templates are saved as page templates.
Classic pages and pages in other folders are copied as files. The home
page becomes the destination's home page only when Replace is checked.

Each file is first copied on the server with SP.MoveCopyUtil.
CopyFileByPath. When SharePoint answers that with a 4xx other than 429,
the file is downloaded and uploaded with Files/AddUsingPath. Files over
250 MB are skipped with a note, and files and pages the destination
already has are skipped unless Replace is checked. Each file gets its own
result, and a failure doesn't stop the others. A page that fails after it
was checked out is discarded, and a page the copy made is moved to the
recycle bin, so nothing is left checked out. Every address uses parameter
aliases, so quotes, # and % in names are safe.

Check these by hand on a real tenant, because they rest on community
sources or are untested: CopyFileByPath between two site collections and
its 403 fallback; parameter aliases in Folders/AddUsingPath and
Files/AddUsingPath; the folder read with $expand=Folders,Files for a
folder that doesn't exist; turning on the AgentAssets feature by REST;
whether a new page comes back checked out; SavePageAsTemplate, the name
it gives the template, and removing the page it was made from; and which
web part settings real pages hold, such as the List and Image web parts.
When the Permissions switch is on, Copy site structure now copies the
permissions of the site into the destination. It maps built-in
permission levels by their type and reports any whose permissions
differ, without changing them. It maps custom levels by name or creates
them, and when SharePoint refuses a new level because one with the same
permissions exists under another name, it uses that level. The owners,
members, and visitors groups map to the destination's own groups by
their role. Other groups that give access to the site or to a copied
list match by name or are created with their settings, then get their
owner (a person through SetUserAsOwner, a group through a CSOM request)
and their members. It gives the same access on the site, and gives each
copied list with its own permissions the same permissions. People
picker columns limited to a group are pointed at the destination's
group.

The copy never removes access or members that the destination already
has. The only exceptions are the access SharePoint gives the person
running the copy on a new group or list, which is removed when that
person doesn't have it in the source. Access and group members that
only the destination has are listed in a note, by display name only.
It leaves out and reports sharing links, the Limited Access System
Group, Limited Access and hidden levels, apps, the source site's own
Microsoft 365 group (every Microsoft 365 group when that can't be
read), and members who no longer exist. Everyone and Everyone except
external users are copied as partly copied, with a note that they open
the item to the whole organization. For a Teams channel site
destination it changes nothing and says that Teams manages those
permissions.

Each item is logged with its own result and the copy keeps going after
a failure; only a stop ends it. Notes never contain sign-in names or
email addresses: a name that looks like one is replaced, and
SharePoint's own messages are cleaned of them before they are quoted.
New destination checks read back permission levels, groups, group
members, access, and lists with their own permissions.

Check by hand on a real tenant:
- SetUserAsOwner and the CSOM request that makes a group another
  group's owner, which only community sources describe.
- Whether creating a group adds the person running the copy as a member,
  and which access SharePoint keeps for them after
  breakroleinheritance(false,true) on a new list.
- The error a person who no longer exists gives when added to a group
  or through ensureuser.
- A subsite destination that uses its parent's permissions: custom
  levels are created on the top site, and the subsite gets its own
  permissions with breakroleinheritance(true,false).
- The SelectionGroup MERGE on people picker columns, which the tests
  can't reach until the list column step fills the column map.
Copy site structure can now start from a data dictionary file (.xlsx) in
the format in docs/data-dictionary.md instead of this site's report. The
file is read in the browser and turned into the same copy plan that the
site report gives, so the copy steps work the same way for both.

From the file, the plan gets each list and library on the Assets sheet,
its columns from the Fields sheet, choices and term sets from Value_Sets
and Value_Set_Definitions, and lookups from Relationships. Each column's
definition is built through the DOM, never from joined text. Columns of
several types are supported: text, multiple lines of text, number,
currency, date, yes or no, choice, lookup, person, hyperlink, managed
metadata (with its hidden note column), and calculated. Files from other
tools that give only display_type values are read too. The term sets of
the site's own term group are copied with their terms, parents, synonyms,
and descriptions; term sets the file lists no terms of keep their IDs and
are never copied. Views, content types, pages, Site Assets, agents, site
skills, and permissions aren't in the file, so they aren't copied.

The file is refused, with a message that says what to do, when it isn't
an .xlsx workbook, is larger than 25 MB, can't be read, has no Assets or
Fields sheet, uses format 2.0 or later, has no lists or libraries, or
holds a hidden-value placeholder such as [phone number hidden] in a name
or choice, or anywhere when the file says values were hidden. Columns the
file can't describe, such as a lookup whose list isn't in the file or
that has no field ID, are left out with a note before the copy starts
that says what to do instead, and are listed for the results table. A
column from another tool whose name SharePoint keeps for its own
columns, such as Order, gets a new internal name, such as Order2, and is
still made. Any unexpected problem while reading the file shows the
message for a file that can't be read.

To check by hand on a real tenant: save a data dictionary with Show
hidden values on, choose it in Tools, and copy to a test site. Then check
that lookup columns show the Title column, that managed metadata columns
are connected to the copied term sets, that calculated formulas were
accepted, and that internal names match the source.
The five parts of Copy site structure (term sets, lists, files, permissions,
and data dictionary files) each passed their own tests, but 11 tests failed
when they ran together. This change fixes the causes and adds tests that run
whole copies.

Fixes for the tests that failed together:
- The pretend destination answered every CSOM request with the first part's
  answer that matched. The lists answer took the request that connects a
  managed metadata column to its term set, which taxonomy.js sends, and saved
  its settings as text. The lists answer now handles only content type column
  requests, and the taxonomy answer handles the connection for list and site
  columns and updates the column's definition, as SharePoint does.
- The permissions tests counted every CSOM request and every change to the
  list. They now count only the permission requests, because the term set and
  column steps now send their own.
- The pretend source site didn't answer two reads the file steps make: the
  site skill's folder and the site's home page. Both are standard answers now.
- tools.spec.js expected the stub results ("isn't available yet") of the term
  set, column, view, Site Assets, and page steps, and expected the term set
  step to send nothing. It now checks the real results, that the term set
  changes come before the list, and that Documents is used as an existing
  library instead of marked as a problem.
- The data dictionary test expected no row for the Title column, which the
  lists step now renames to match the source.

Shared changes requested by the module builds:
- runner.js: checks each list with snaToolsListChoiceProblems, so a copy into
  a library the destination already has no longer stops before it starts;
  adds the Skipped rows of a data dictionary file with
  snaToolsLogWorkbookSkipped; adds fileUrls and fileIds to the maps.
- plan.js: recomputes a data dictionary file's warnings when the person
  chooses lists and term sets, so a lookup to a list they left out is named
  before the copy; describes lookupFieldGuessed and skippedItems.
- ui.js: counts a managed metadata column once, without its hidden Note
  column.
- lists.js: adds a note to a lookup from a data dictionary file that shows
  the Title column; when permissions are copied, a Person column limited to
  a group is no longer marked as partly copied, because the permissions step
  sets its group.
- taxonomy.js: connects a site column to its term set too.

New tests in tests/tools-integration.spec.js copy a site with two lists, a
lookup, a managed metadata column, a calculated column, a formatted view,
Site Assets, a page, an agent, a site skill, and permissions. They check the
order of the changes, the results, and the check afterwards, including an
item the destination doesn't have afterwards. Two more tests copy with
Permissions off and from a data dictionary file. docs/development.md
describes the new tests and how the parts share CSOM requests.
A CSOM error about one term, such as a label SharePoint doesn't accept,
no longer stops the rest of the term store copy. Only an access denied
answer, HTTP 401 or 403, or a "Cannot find stub for type" answer before
CSOM has worked once marks the term store as blocked.

A reused term is made unavailable for tagging only when it is unavailable
in the term set being copied, not in another term set it also appears in.

When the REST API refuses a new term set's open or tagging setting, the
copy now sends that setting through CSOM and uses CSOM for the rest of
the copy, as it does for every other refused change.

Term set notes now show language names, such as French (France), instead
of language codes, and the note about a term set that couldn't be read in
full says what to do next. The error for a value that can't be sent now
says what to do instead of describing the code.
A failed read of one list's access in this site no longer stops the
whole Permissions step. That list keeps the site's permissions in the
destination, and its row says how to give it its own by hand.

When a list gets its own permissions, the copy now reports access that
SharePoint kept on it and that this site doesn't give, such as a group
the person running the copy is in. The list's row is marked Partly
copied when some people or groups didn't get their access, or when the
copy couldn't read the list's access afterwards to check.

Groups that a copied people picker column is limited to are now copied
even when they give no access, so the column points at the same group
in the destination.

A new group's owner follows the same rules as members and access: this
site's Microsoft 365 group, apps, and SharePoint's own account are left
out, and the note says how to choose an owner.

The note for a built-in permission level the destination lacks now
gives a step the person can take, and the note for missing levels
handles one level and several.
Add snaToolsRewriteSourceLinks to core.js. It changes links to the source
site in every stored form (absolute, server-relative, URL-encoded once or
twice, JSON-escaped, and HTML-escaped) and source list, view, column, term,
file, site, and site collection IDs in every form. It keeps boundaries, so
look-alike sites and other hosts stay as they are. Lists copied under a new
address get the new address. Subsites, sharing links, IDs with no match, and
lists the copy didn't make are returned as unmapped, so the page is marked
Partly copied. Navigation can use the same helper later.

Copied pages now use it in place of the local rewrite in files.js. A modern
page in a folder is copied on the server, and then its links are updated
through the site pages API. It is never uploaded, because an upload leaves
the page blank.

Fixes from the review of the files module:
- Copy page templates before other pages, so a page with the same name
  doesn't block its template.
- Send IF-MATCH and X-HTTP-Method with SavePageAsTemplate and
  SavePageAsDraft, as the CLI for Microsoft 365 does.
- Copy an agent saved in Site Assets into the destination's Site Assets,
  and make that library when the destination has none.
- Add files the destination already has to the file maps, so pages point
  at them.
- A Site Assets folder that can't be read fails on its own row, and a failed
  read of the destination's lists fails only the agents, not the site skills.

Add tests/tools-links.spec.js with the cases from the link research and its
review, and tests in tools-files.spec.js for each fix, including a home page
with many link forms.
Copy site structure now copies the source site's own content types,
whether or not a chosen list uses them. The Tools screen has a Content
types part with a checkbox for each one, all checked at first. A content
type that a chosen list uses, or that another chosen content type is
based on, stays checked and says why. A data dictionary file has no
content types, and the part says so.

Each copied content type gets its columns through CSOM with the same
Required, Hidden, and display name settings as the source, such as
Enterprise Keywords shown as Tags. Site columns that only a content type
uses are copied first. SharePoint's own site columns that were renamed
in the source get the same name in the destination, with a result row
of their own. Content types from the content type hub are reported
with what to do instead of being made as local copies, and so are
content types based on them.

The review fixes:
- A view named All Items is now made when the source default view has
  another name.
- A list the destination already had keeps the formatting of its
  built-in columns.
- SharePoint's own site columns that someone added to a list, such as
  Start Date, are added from the destination's site columns.
- A new list keeps its Item content type when its own content type
  couldn't be added.
- Views leave a skipped column out of their sorting, grouping, filters,
  and totals, and say so.
- A site column that shows another column of a lookup gets a Skipped
  row.
- The note for a list the destination already had says what happened.
When the REST API refused a new term set's settings and CSOM refused
them too, the copy marked the term store as blocked. Every later term
and term set then failed without being sent, even though the REST API
had just created a term set. The same happened when the REST API and
CSOM both refused one term's settings.

Now the term store is marked as blocked only when CSOM is refused
before CSOM or the REST API has worked in this copy. Otherwise only
that change fails, and later changes go through the REST API again.
A new term set's settings switch the copy to CSOM only when CSOM
makes them.

The note for a term set that isn't complete now says the data
dictionary file doesn't list every term when the copy comes from a
file, instead of blaming a site that doesn't exist.
When a list gets its own permissions, SharePoint keeps access for the
person running the copy or for a group they are in. If this site gives
that person or group less access, the extra permission levels were never
reported. Each person or group's row now names the levels they also have
in the destination that this site doesn't give them, and the row is
marked Partly copied. The same check runs for access to the site. For
the person running the copy on a list, the copy removes those extra
levels when it gave them every level this site gives them, and otherwise
reports them.

Notes about a new group's owner now say "the same owner" when the owner
has no display name, instead of sentences such as "with its owner as its
owner". A people picker column limited to a group that no longer exists
in this site now says so and what to do. The note for an owner left out
because the site's Microsoft 365 group couldn't be read no longer
repeats "Sitenalyzer".

The architecture page no longer says that the Tools files for data
dictionary files, term sets, files, and permissions are not built yet.
SharePoint's own page and media content types are now recognized by their
full IDs. A content type someone based on Site Page, Rich Media Asset, or a
publishing page used to count as SharePoint's own, so the Tools screen didn't
offer it and the copy dropped it without a note.

A list content type that comes from SharePoint, such as Event, now gets a
Skipped row when the destination list doesn't have it. The copy matches these
by ID as well as name, so Item under a name in another language is kept and
not removed from a new list. Folder and hidden content types aren't reported.

The hidden Note column of a managed metadata site column is no longer named in
a content type's note. When the copy makes the site column, the content type
gets the Note column too; when the destination already has the site column,
the copy uses the destination's Note column with the same ID or internal name.

When a new content type's columns can't be read in the destination, the copy
now uses the columns of SharePoint's own content type it's based on, so
SharePoint site columns such as Enterprise Keywords are still added. When that
parent isn't known either, the note names the columns to check.

Only SharePoint site columns that someone added, such as Enterprise Keywords
or Start Date, get their source names in the destination. Core columns such as
Title, and columns a content type gets from its parent, keep the destination's
names, so a source site in another language no longer renames them across the
destination's site collection.
Copy site structure now reads the source site's subsites before it
copies pages, so a link into a subsite stays as it is and the page is
marked as partly copied, instead of pointing at a destination subsite
that doesn't exist.

When the source is a subsite, the site collection ID in page content
is changed only next to the source site's own ID. A web part that
points at the parent site keeps its site collection, so it still opens
the parent site's content, and the page is marked as partly copied.
Sitenalyzer finds out whether the source is a subsite by reading the
site collection's top site. When that read fails, it treats the source
as a subsite, which never leaves a web part pointing at nothing.

A page template whose temporary page couldn't be removed no longer
makes the real page with the same name say the destination already
has it. That page now fails with a note that says to delete the
temporary page and copy again.

The note for a site skill now names a folder that couldn't be read
apart from the files that couldn't be copied, instead of counting the
folder as one file. The note for an agent whose destination lists
couldn't be read now gives advice that fits the error. A page in a
folder, or a classic page, that replaced one in the destination now
says it was replaced.

The pretend source site in the tests answers the two new reads. The
architecture and development pages now describe files.js, the link
rewrite in core.js, and tools-links.spec.js.
Copy site structure has two new switches under Other content. Navigation
is on by default for a site source. It copies the left menu, the top menu
of a classic site that isn't a hub, and the footer's links. Links stay in
order with their labels, hidden links, and audiences, and audience
targeting is turned on in the destination when this site has it on. Every
link into the source site is pointed at the destination through
snaToolsRewriteSourceLinks, and links to other sites stay the same. Each
menu is made the same as the source's: the destination's own links are
removed only after the source menu was read, so a second copy adds no
link twice. A hub menu is reported, not copied.

Site look is off by default. It copies the theme colors with ApplyTheme,
the header, menu style, and footer settings with SetChromeOptions,
whether the site navigation shows, the site logo and its thumbnail (the
group picture on a site with a Microsoft 365 group), and the footer's
display name and logo. Pictures are copied into the destination's Site
Assets first. Each setting gets its own result row, and parts SharePoint
refuses because custom scripts are off or a SharePoint admin is needed
are skipped with what to do instead. Both switches are disabled for a
data dictionary file.

The new code is in src/tools/site.js, built after files.js. The runner
copies the site look after Site Assets and the menus after Site pages,
both before Permissions, and the check afterwards reads both back. New
tests are in tools-site.spec.js with a pretend destination for menus and
look. The integration test now copies a left menu too, and the tools.spec
copy with the other parts off also turns Navigation off.
When the destination already had a list with its own permissions, such as
on a second copy into the same site, the permissions copy removed levels
from the person running the copy, and their whole access, even though they
had that access before the copy. The notes then said SharePoint gave them
that access. The copy now reads whether each destination list already has
its own permissions before it changes anything. It removes access only
from a list that used the site's permissions, because only then did
SharePoint give that access during the copy. A list that already had its
own permissions is left as it is and only gets the access this site gives.
When the copy can't read this, it removes nothing and says what to check.
Lists now get their own permissions without clearing the permissions of
their items, so a list the destination already had keeps them.

The note about a level that a person has only in the destination now says
"they" instead of "it" for a person.

The destination check replaced the whole note of a permission row when it
found a missing level, which erased the notes about extra levels and about
opening the site to the whole organization. Those sentences are now kept
after the check's own note.
When Sitenalyzer couldn't read a new site content type's columns in the
destination, it used the parent's columns only when the parent was one of
SharePoint's own or was in the copy. A content type based on a content type
hub type, such as Policy Document, got a note that named every column it has,
including hidden ones such as Source Url and the Content Type column, which
people can't see in the content type's settings.

The plan now keeps the columns of every parent the source site lists, so the
copy uses them for hub parents too. When the parent isn't known, the note
leaves out hidden SharePoint site columns and the Content Type column.

The Skipped note for a SharePoint content type the destination list lacks now
says what happened: the destination list doesn't have it, and Sitenalyzer
doesn't add those. The old note gave a circular reason.
When the source is a subsite, Copy site structure now finds the site
collection ID in page content in every form the link rewrite finds it,
such as encoded twice or with %252D between its parts. Before, those
forms were missed, and the page said Copied while it still held the
source's ID. The page is now marked as partly copied while the old ID
is still in it in any form, found by its 32 hex digits.

The part of the text checked for the destination site's own ID now ends
at an HTML tag edge, and for an ID in a link, such as ?siteid=, at the
end of that link. So a link to the parent site that follows a link to
the subsite itself keeps its site collection ID instead of getting the
destination's. Braces right around any ID, such as the list ID of a web
part item, no longer count as the edge of that part, so an item that
lists its site ID last is changed as it should be.

The GUID helpers of the link rewrite are now shared functions in
core.js, so the files copy uses the same rules. The note for a page
blocked by a page template's temporary page is split into shorter
sentences.
Copy site structure could leave menus and the site look in a different state
than its results said. This change fixes those cases and reports the ones
SharePoint doesn't allow.

Menus:
- A link added with CSOM after the REST API refused it is now hidden and given
  its audiences with a change that leaves out its title and address, which
  SharePoint could refuse again. If SharePoint still refuses, the link is
  reported as partly copied and showing to everyone, instead of failed.
- The save through the menu editor now sets each copied link's hidden state
  as well, and reads the menu back to confirm it, so a hidden link can't become
  visible without a row saying so.
- When this site has audience targeting off, links are copied without their
  audiences, so they still show to everyone. All copied menus are checked for
  audiences, not only the left menu.
- Links to the group's notebook, Teams, and Planner stay as they are and are
  reported as still pointing to the old group. Links kept as they are, such as
  # or SitePages/Home.aspx, now say so instead of claiming to point outside
  this site.
- Top menu and footer links that open in a new tab are reported, because
  SharePoint keeps that setting only for the left menu.
- A removal that stops partway says how many links are gone. The row about
  removed links is no longer counted as a copied item.
- A menu with more than 500 links is reported instead of being copied short
  without a word. A footer that couldn't be read is reported apart from one
  SharePoint didn't let Sitenalyzer set up.

Site look:
- Footer settings are copied only when this site also has a footer, so a team
  site no longer turns off a communication site's footer.
- When the destination's header and footer settings can't be read, nothing is
  sent, because SharePoint changes them all at once.
- A group site's logo is reported as copied when only reading it back failed,
  and the copy goes on to the thumbnail.
- When SharePoint refuses to change a group's picture, the logo is saved as
  __siteIcon__.png in Site Assets, which Microsoft documents as the way around
  that refusal.
- The theme row says that the colors of page sections might differ, and the
  Site look hint is one sentence.
Copy to destination now runs a read-only check of the destination before the
copy starts, so people learn about failures they can predict, such as term
sets their organization won't let them create, before anything changes.

The checks live in the new src/tools/preflight.js, which the build places
after verify.js in both build.sh and build.ps1. Each check is added with
snaToolsAddPreflightCheck and returns warnings that say the item will fail,
will be skipped, may fail, or are a note. The checks cover term sets the
destination can't take (naming every column that uses them), columns whose
term set or lookup list was left out, calculated columns that use a skipped
column, term sets of the organization missing from the destination's term
store, content types from the content type hub, a missing Agent Assets
library, Site Assets files over 250 MB, classic pages and site look settings
on a destination without custom scripts, a Teams channel site with
Permissions on, and a list address the destination now uses for another kind
of list. Reads go through the existing read helpers; the one CSOM request
only loads the term store and is refused by its sender if it could change
anything.

The window.confirm prompt is replaced by a "Before you copy" part of the
Tools screen with a summary, a table of warnings, and Copy anyway and Go
back buttons. After the copy, the warnings appear at the top of the results
table with the result Warning, and the counts sentence mentions them without
counting them as copied, skipped, or failed. The lookup and term set
warnings that plan.js used to add are now part of the check.

Tests: tools-preflight.spec.js covers each check, Copy anyway, Go back, a
check that fails, keyboard use, and that the check sends no change. The
tests that accepted the old confirmation now select Copy anyway.
Add a task page, docs/how-to/copy-a-site.md, that walks through Copy
site structure: who can use it, choosing a source and a destination,
the lists and libraries table with its Name and Address boxes, term
sets, content types, each switch under Other content, Replace, Stop
copying, the results table, the destination check after a copy, and
copying again into the same destination.

Update the other pages to match the code as it is now. usage.md gets a
Tools section with the #sna/tools and #sna/tools/copy addresses.
architecture.md adds site.js to the file table, the requests that Tools
sends when it opens and for Navigation and Site look, and the full copy
order. design-decisions.md explains the main choices, such as the copy
order, creating lists with the address first, Permissions off by
default, and the few cases where the copy removes something.
troubleshooting.md lists six problems the copy reports, each with what
to do. data-dictionary.md describes how a data dictionary file is used
as the source, how lookups are matched, and which files are refused.

Also add the page to the docs index and the README, add site.js to the
build order in development.md, and add the Tools terms for site
collection administrator, SharePoint admin, Navigation, Site look,
Replace, results, and the destination check to the project terms.
When the destination already had a list that the copy used, and that list used the site's permissions, the copy gave
it its own permissions with only the access of the person running the copy. Everyone else lost access to it, such as
the destination's members and visitors groups on the Documents library. The lists step now records each list it used
instead of creating, and the permissions step gives such a list its own permissions with a copy of the site's access.
It then reports the access this site doesn't give instead of removing any of it, including the access of the person
running the copy.

The row for a list that already had its own permissions no longer says the access was added before any was given.
It now says only that no access was removed, and the rows for each person or group say what was given.

The Other access row no longer names the person running the copy in the third person. On a list, the Permissions row
tells them about their own access. On the site, the Other access row does. When the row names one person, it now
says "they" instead of "it".

The guide and the design decisions page now describe how the copy treats a list the destination already had.
When the destination already had a list that uses the site's permissions, the copy gave that list its own permissions
only after it had given people access to the destination site. The list then got everyone the copy had just added to
the site, such as Everyone except external users, even when this site limits the list to a few groups. The results
said this access was already there, so the person had no reason to remove it.

The permissions step now gives each such list its own permissions before it gives anyone access to the site. The list
keeps the access people had to it before the copy, and nobody gets access to it only because the copy gave them access
to the site. The list's row now says it kept the access anyone had to it before the copy.

The test for this case now checks that the groups the copy adds to the site get no access to the list, and that the
members group keeps only the level it had before the copy. The guide and the design decisions page now describe the
new order.
On a subsite, the site's own content types leave out the ones it gets from the sites above it, such as Document. When a copied content type was based on one of those and the destination couldn't list the new content type's columns, the copy didn't know which columns came from the parent. Its note then named the parent's hidden columns, such as _SourceUrl and TemplateUrl, as columns to check.

The copy plan now reads the site's available content types when a copied content type's parent isn't among the site's own, and uses that parent's columns. When that read fails too, the copy works as before and names the columns it couldn't check.

A new test copies a subsite content type based on Document while the destination refuses to list its columns, and checks that Start Date is added with no note. The existing test for the note now refuses the available content types read, so it still reaches the case where the parent is unknown. The architecture page lists the new read.
When the source is a subsite, Copy site structure changes the site
collection ID in a page only where the subsite's own ID is in the same part
of the text. For an ID that follows = in a link, that part ended at the
nearest space or quote, so a page with an item such as a Quick links entry
with siteId, webId, and a link ending in ?siteid=<id> was marked Partly
copied even though the item named the subsite. The shorter part is now used
only when the link itself names a site with webid= or guidWeb=. Otherwise
the whole item or HTML tag is checked, as before.

A link now also ends at a quote written as a code, such as \u0022, %22, or
%2522. Before, a link to the parent site that followed a link to the
subsite in that form got the destination's site collection ID while
keeping the parent's site ID.

Two tests cover these cases, and both fail without the change.
…e item

When the source is a subsite, Copy site structure changes the site
collection ID in a page only where the subsite's own ID is in the same part
of the text. For an ID that follows = in a link, the part ended at the
nearest space or quote, and when that part didn't name a site, the whole web
part item was checked instead. A query such as SiteId=<id> WebId=<parent
site's ID> in an item that also held the subsite's ID therefore got the
destination's site collection ID while still naming the parent site, so the
query pointed at a site that doesn't exist and the page was marked Copied.

Now the text up to the nearest quote is checked before the whole item. A
query that names the parent site keeps its site collection ID, and the page
is marked Partly copied. A query that names the subsite still changes, and a
lone ?siteid=<id> link still uses the whole item.

A new test covers this case and fails without the change.
A hidden link with audiences that the REST API refused was added with CSOM
and then changed once, with both its hidden state and its audiences. A
tenant that refuses audiences refused the whole change, so the link stayed
visible. The copy now hides the link and gives it its audiences in two
separate changes. The left menu's read-back checks the two apart, and the
link's row and destination check follow what SharePoint confirmed.

When SharePoint refuses to change a group's picture, the copy saves the
logo as __siteIcon__.png in Site Assets. It now does this only for a PNG
logo, and never replaces a __siteIcon__.png the destination already has.
In that case the row says how to change the site logo thumbnail by hand.

A group site's logo whose read-back failed no longer gets a destination
check, because changing the group's picture afterwards can change the logo.

When the communication site feature can't be read, the copy no longer
treats the site as one without a footer. For this site, the footer links
are still copied, and the footer settings row says Sitenalyzer couldn't
check. For the destination, the footer and the menu orientation are left
alone, with rows that say why.

When this site's settings can't be read, the row about audiences now says
Sitenalyzer couldn't check audience targeting, instead of saying it is off.

The group notebook check now matches only a Site Assets folder whose name
ends in " Notebook", with or without a slash at the end, and the note for
these links uses one name for the group.
When SharePoint refuses to change a group's picture, the copy saves a PNG
logo as __siteIcon__.png in Site Assets, and never replaces a file that is
already there. On a second copy, that file is the one the first copy saved,
but the row still said the thumbnail was skipped and asked the person to
upload the copied logo by hand.

The copy now reads the existing __siteIcon__.png and compares it with the
copied logo. When they match, the row says Copied and that Site Assets
already has the copied logo under that name. When they differ, or the file
can't be read, the row keeps the note that says how to change the thumbnail
by hand. New tests cover a second copy and a file that can't be read.
The check before copying now matches what the copy does, so Before you copy
no longer says the copy is clear when it will fail or stop.

- When the destination already has its own term group, the term set check
  still reads the term store through CSOM and warns that changing term sets
  may be refused, because a group doesn't mean the person may change it.
- The Lists and libraries check runs the same name and address checks as the
  copy, so a name the destination started using after Check destination is
  found. A problem that stops the whole copy is marked as such: Before you
  copy then says the copy can start only after the problem is fixed, offers
  only Go back, and Lists and libraries marks the box to change.
- A classic page the destination already has isn't listed as failing when
  Replace is off, because the copy leaves that page as it is.
- Managed metadata columns with no term set, and the columns a data
  dictionary file can't make, are listed as skipped, with the calculated
  columns that use them. The copy now skips those calculated columns too,
  instead of sending formulas that use missing columns.
- Messages say "your term store administrator" everywhere.

The destination check now refuses this site, by its ID when both IDs are
known and by its address otherwise, because a copy into this site would
change it. Replace is off and grayed out for a data dictionary file, which
has no pages or files. A failed read of this site while the copy is prepared
gets its own message instead of one about the destination. The unused
SNA_TOOLS_NOT_READY_NOTE and a plan warnings call that did nothing for files
are removed.

Tests cover each change, including that the CSOM read refuses a request that
could change something. The how-to, troubleshooting, architecture, design
decisions, data dictionary, and usage pages describe the new behavior.
Check destination refused the site whose report is open even when the source
was a data dictionary file. None of the reasons for that refusal apply to a
file: navigation, site look, and permissions are off, and no list of this site
is the source. Someone building the lists of a new, empty site from a file
could not copy into it.

Check destination and the check at the start of the copy now refuse this site
only for a copy from this site's report. For a file copy into this site, the
hint under Copy says the copy goes to this site instead of saying nothing
changes in it. When a check found this site, or is still running, changing the
source drops that check and asks the person to check the destination again.

The design decisions and architecture pages now say that Check destination
refuses this site, since "destination check" means the reading back after a
copy. The how-to, troubleshooting, usage, and data dictionary pages describe
when this site can be the destination. A new test copies a file into this site
and checks that the same site is refused for this site's report.
Add a "Before you copy" section to the how-to page. It describes what the
check reads, what will fail, will be skipped, and may fail mean, the Copy
anyway and Go back buttons, a problem that stops the copy and leaves only
Go back, checks that couldn't finish, and the Warning rows that the check
adds to the top of the results. The usage and troubleshooting pages now
describe the same steps, and the terms list adds "Before you copy" and the
Warning result.

Check every statement about Tools against the code and fix the ones that
didn't match. The pages no longer describe a confirmation question before
the copy. The destination messages are quoted in full, the data dictionary
switches and the tests of tools.spec.js and tools-preflight.spec.js are
described as they are, and the architecture page lists the reads that the
check makes. The design decisions page no longer gives a reason for
Permissions being off at first beyond what the switch does, and no longer
gives reasons for the site collection administrator check or the read back
after the copy that the code doesn't support.
A page that named a later page by its ID, such as in a Quick links web
part, was saved with the source page's ID, because that page's
destination ID didn't exist yet. After every page is copied, Sitenalyzer
now saves such a page again with the destination ID and updates its
result. A page still holding something of this site stays Partly copied,
and a page SharePoint refuses to save again says so in its note.

When the copy of Site Assets left the destination's own file at the
logo's address, the Site logo and Footer logo rows said Sitenalyzer
copied this site's logo. Those rows now say Sitenalyzer used the picture
the destination already had, and how to replace it.
When the destination is a subsite, the copy now checks whether each of its owners, members, and visitors groups is also one of those groups on its parent site or on the top site of its site collection. SharePoint groups belong to the whole site collection, so adding people to such a group would also give them access to those sites. The copy no longer adds anyone to such a group. It copies this site's group the way it copies other groups: to a group with the same name, which it makes when the destination doesn't have one. When Sitenalyzer can't read the other sites' groups, it treats every one of the destination's groups as shared. Before you copy lists each shared group, and the pretend destination can now act as a subsite of a parent site so the tests can cover this.

The permissions step now limits only the Person columns that this copy made. A Person column that the destination already had keeps its own group, and its row says which group this site uses. When the permissions step stops early, such as for a channel site or after a failed read, each Person column the copy made gets a Partly copied row that says it lets people choose anyone and what to do. The column's first row now says that it lets people choose anyone until the groups are copied.
Notes that quote SharePoint's own error message now replace every sign-in
name and email address with "the account", and hide other values that look
like personal information. This happens in one place, snaToolsServerSaid in
core.js, so the notes of every step are covered, and the Permissions step
uses the same helper instead of its own copy. Saved results files also hide
such values in the destination's name in the file name, as the data
dictionary and permissions workbooks do.

After Stop copying, the copy still adds rows until it ends, so a change that
SharePoint finished after the stop appears in the results. A request whose
request digest was still on its way when the person stopped is no longer
sent. When a stop leaves an item partly made, its row is Partly copied and
says what to do next: a list without its chosen name or settings, a blank or
checked-out page or page template, or a menu with only some of the
destination's links removed. Any other change that has no row yet gets a
Partly copied row for the step that was running. The results then say that
the item being made is marked Partly copied.

The destination check keeps the sentence about the hub when it finds other
theme colors in a destination connected to a hub, and its note for items it
couldn't check reads correctly for one item and for several.

The how-to, architecture, and development pages describe the new behavior.
A copied agent still uses the knowledge sources in this site. Its note now says so with the project's term for the source, and tells the person to edit the agent in the destination to use the destination's lists and libraries instead.
Copy site structure no longer replaces the New menu order of a list or library
the destination already had. It used to set that order from the source list
alone, which took the destination's own content types off the New menu. Now it
adds the content types it copied to the end of the list's own order, and leaves
a list without an order of its own as it is. When it has to turn on Allow
management of content types for such a list, the list's row says so.

SharePoint's own site columns, such as Enterprise Keywords shown as Tags, are
renamed in the destination only when this site and the destination use the
same language. When the languages differ, the column keeps its name and its row
says why. The row for a renamed column now says that the new name shows in
every site of the destination's site collection and how to undo it, and Before
you copy lists each column the copy will rename.

The note under the Address box of a list or library the destination already
has, and a new row in Before you copy, now say when the copy will rename it to
the name in the Name box.

A lookup site column in a site content type is no longer reported as Failed in
each list that uses the content type. Adding the column to the content type
also adds it to those lists, so the copy now reads each list's columns after
that, and the column's views and the columns that show another column of it
keep it.

The pretend destination in the tests now answers the New menu order of a list,
the language of the top-level site, and adds a site content type's columns to
the lists that use it, as SharePoint does. The how-to guide and design
decisions describe the new behavior.
Content type names in Tools are now hidden like every other value the first
time the Content types part is drawn, so a name that looks like a phone number
no longer shows until a later redraw.

Use this site in Choose a site now keeps the dialog open and says what to
enter when the address box is empty, instead of clearing a destination that
was already checked. Typing another address or choosing another site while a
destination check runs now drops that check's answer, so it can't answer for
an address that is no longer in the box, and Check destination works again
right away.

Empty status lines now stay on the page instead of being hidden, and the
messages written after the view is drawn again (Go back, a source change, and
a file that was read) are written a moment later, so screen readers announce
them. The table of lists now becomes a keyboard-scrollable region whenever its
box changes size, such as after zooming in, not only when it is first drawn.
The headings under Copy site structure, Before you copy, and Results are now
one level under the part heading.

While a copy runs, Refresh report, Start over, and Change site are turned off,
with a sentence under them that says why, and leaving or reloading the page
makes the browser ask first. Each of them would otherwise stop the copy and
clear its results without asking.

The check before copying now counts a term store it couldn't read as a check
that couldn't finish, reads the destination's content types again before it
warns about content types from the content type hub, and no longer says the
destination lacks the list of a lookup column from a data dictionary file,
which it doesn't check. When the whole check fails and the person copies
anyway, the results now start with a row that says so.

The tests cover each change, including that Navigation and Site look are off
and disabled for a data dictionary file. The how-to, architecture, and design
decisions pages describe the new behavior.
When the copy doesn't know a lookup column's source list, as with a data dictionary file, it never looks in the destination for that list. The skip note now says only that the list isn't in this copy, instead of also saying the destination doesn't have it.
The files in dist/ are built from src/ with build.sh, so a site owner can upload them and test Copy site structure on a SharePoint site.
Copilot AI balanced review requested due to automatic review settings November 9, 2026 17:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T17:55:07.106698Z 9f23b9a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f23b9a164

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread src/tools/site.js
Comment on lines +462 to +463
var options = snaToolsFilesLinkOptions(plan, dest, maps, fileState, sourceLists, destLists);
options.wholeLink = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load subsite boundaries independently of copying pages

When Navigation is enabled but Site pages is disabled, fileState.subsites remains empty: its only initialization from SharePoint happens in snaToolsCopyPages, which also returns before that initialization when the source has no pages. Consequently, navigation rewriting changes a link such as /sites/source/archive/SitePages/Home.aspx into /sites/destination/archive/SitePages/Home.aspx, reports it as copied, and loses the working link to the source subsite even though subsites are not copied. Load the source topology before constructing the navigation link options, independently of whether pages are copied.

Useful? React with 👍 / 👎.

Comment thread src/tools/ui.js
Comment on lines +855 to +857
var plan = await snaToolsReadWorkbook(file);
if (_snaToolsState !== state) { return; }
state.workbook = { fileName: file.name, plan: plan };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject results from superseded workbook reads

If someone selects workbook A and then selects workbook B before A finishes reading, both requests retain the same state, so this check accepts both results. If A finishes last, it replaces B's copy plan and redraws the choices for the wrong file. This can cause the destination to receive a different structure than the person selected. Track a file-read generation or the selected file and discard superseded results in both the success and error paths, as the destination check already does.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add ability to copy sites and import site definitions/dictionaries into a site

2 participants