Folders
Folders put books into a tree and let the books in a tree inherit settings. This page follows the feature through the layers: the entity, how a setting is resolved, the services, the lists and windows, and the places that had to learn about folders (backups, agents, the help assistant, cleanup). The user's view is in Folders.
The data
Folder (domain/model/impl, table folders, an ExtendableEntity, so it has an owner, a uuid, soft delete and the
extended content) is:
Manuscript.folder is a nullable ManyToOne column folder_id (null is the top level), annotated @CleanupReference(WEAK): a
purge of the folder unlinks the books that are still alive instead of deleting them. The migration is V21__folders.sql
(table, sequence, manuscripts.folder_id and the indexes (parent_id, is_deleted), (userId, is_deleted) and
(folder_id, is_deleted)). persistence.xml lists the entity, services-config.xml the repository and the service.
The limit of the version history is a setting of the folder too, historyMaxVersions (an Integer, null is not set), carried by FolderService.Details (its four-argument constructor leaves it null, which clears it) and copied by copyTree.
The settings are extended attributes and not columns because nothing queries them in SQL; the price is the usual one, they
are persisted only by FolderService.save/update. The name is a column because the lists sort and filter on it.
Two more extended attributes of UserSetting serve the feature: favoriteBooks (the uuids of the favorite books) and
helpAssistantBooks (the opt-in of the help assistant). UserSetting.defaultStoryLayout is not about folders but arrived with
the same work (see the story layout).
Resolving a setting
A book's setting is the first non-blank of: the book, the folders above it (nearest first), the UserSetting, the built-in
default (Defaults). The code is in three places:
-
PromptField(domain/service) is an enum of the twelve text settings (MASTER_TEMPLATE,POV,TENSE,STYLEand the eight prompts). It holds the getters of each level (ofBook,ofFolder,ofUser), the setters (setOfBook,setOfFolder, which store a blank as null) and the built-infallback(). A new inheritable setting is a new constant here plus the field onManuscriptandFolder, and nothing else in the resolver. -
EffectiveSetting<T>(value, origin, folder)is what the resolver returns: the value, where it is set (OriginisBOOK,FOLDER,USERorDEFAULT) and the folder when it isFOLDER.isCustom()is true for the book and the folders. -
ManuscriptService.resolve(book, field)andresolveLanguage(book)do the lookup;resolveInherited(folder, field)andresolveInheritedLanguage(folder)start from a folder (what an empty field would inherit, for the placeholders of the dialogs). The old getters (getPov,getStyle,getTemplate...) keep their signatures and returnresolve(...).value(); they were twelve copies of the same lookup before.getLanguage(book)is the resolved language,Manuscript.getLanguage()is still the book's own or English, andManuscript.getOwnLanguage()is null when the book sets none.
FolderService.findChain(folder) reads the folder and its ancestors by id (folder first, stopping at a deleted one), with no
current user, so a generation can resolve from a worker thread. The lookups are done when needed and nothing is copied into
the book, so moving a book or editing a folder changes the next generation. A resolve of all twelve settings and the language
costs about 1 ms with no folder and 3 ms through ten folders (measured on the test database), a generation does a handful of
them before an LLM call, so there is no cache; add one per generation only if the chain gets much longer or the lookups end
up in a loop over many books.
Everything that used the book's fields directly now goes through the resolver: the generation steps, the summaries, redraft,
annotate and impersonate, the story export (language), the reader, the assistant commands (get-book-info and the book
settings commands report the effective values) and the help assistant.
The services
FolderService (ExtendableService<Folder, FolderRepository>, see Services):
Errors: IllegalArgumentException for what can never be right (a blank name, a folder that is not the user's),
IllegalStateException for what is wrong now (the name is taken, a folder into itself), ManuscriptLockedException /
FolderBookLockedException for the agent lock. ManuscriptService.moveToFolder(books, folder) moves books, save checks that the folder
of a book is its owner's, and findRegularInFolder, getFavorites and setFavorite serve the lists.
Transfers
transfer takes a Transfer(books, folders, target, copy, onlyCurrentBranch, choices) and checks everything first: the target
and every item belong to the user, a folder does not go into itself or below itself, every clash has a Choice (RENAME,
SKIP or MERGE, keyed by the id of the folder; findClashes tells which need one), and no book (also in a moved or copied
folder) is locked by an agent. Then:
- a move is one transaction;
- a copy has none. It can take long and the database has one writer, so each folder made and each book cloned
(
BackupService.cloneManuscript(..., folder)) is a step of its own, and thestepcallback is called after each (the progress bar ofOrganiseDialog). A failure leaves the copies already made, each complete. This is the pattern of the other long tasks: transactions per step, never around the lot.
The result is an Outcome(books, folders, skipped).
The modification time
Folder.modification follows the books below it. JpaManuscriptRepository.changed(book) (the hook of
JpaBaseRepository.write, which the repositories override to tell the owner of an entity; JpaChatMessageRepository.touchBook does
it for the parts) calls FolderModification.touch, which walks the parent chain and sets the time with one UPDATE ... WHERE id IN.
It skips agentic working copies. saveWithoutModification, opening a book and moving a book change nothing; the active-leaf
change (swipeAndSwitch, branchAndSwitch) saves the book as it did, so it counts.
Stale copies
The dialogs keep entities for as long as they are open, and a generation held its book from start to finish, so saving such a copy
wrote old values over a setting changed in the meantime (or undid a move to a folder). The writers that change one thing now call
ManuscriptService.update(book, BookChange), which loads the stored book and applies the change to it. Use it instead of save
for a change that is yours alone.
The lists and windows
All the code is in ui (the classes are @Configurable, the windows @Extendable with @PublicAPI, see
Extendable classes):
Plugins that name a changed method or variable must follow @PublicAPI, see Extendable classes.
Other places that know about folders
-
Backups.
BackupServiceImpl.takeBackupwrites the optional keyfolder(the folder's uuid and thepathfrom the top, each with name, uuid, description, language, text settings and, asextensions, the values of the extension settings by key, kept whether the extension is loaded or not) fromfindChain;BackupFolders.resolvefinds the folder by uuid, else by the name path (creating the missing levels with the saved settings), else returns null (the top level, also on any failure).createBackup, which the agents' snapshots andcloneManuscriptshare, is unchanged and writes no folder.exportBackup(backup, withImages, includeInherited, progress)rewrites the stored file on its way out throughBackupInheritance: it dropsfolderand, with the option, writes the values the book inherits (from the backup's own folder path, then the user settings, never the built-in defaults) as the book's own; the settings of extensions too (InheritedSettingRegistry): a value the book does not set comes from the nearest folder that sets it, else from the user level of the extension (a registered setting, and only when the user settings are written), and the values the book sets itself go along, because they are stored in one object that replaces the book's. The old three-argument overload exports with the option on. -
Agents. The working copy (
agentic = true) used to have no folder and so ignored the folders' settings. It is put in the folder of the original:createAgenticCopy,replaceWorkingCopy(restore of a checkpoint) andAgentMergeServiceImpl.synccallManuscriptService.setAgenticCopyFolder. A merge is a diff of JSON without a folder, so it never moves the book. The lock isAgentRunService.checkNotLocked, applied by the folder service to every book below a folder that would change (FolderServiceImpl.checkNoLockedBook); a paused or stopped run does not lock. -
Help assistant.
HelpAssistantService.isBooksAllowed/setBooksAllowedkeep the opt-in. When it is on,askaddsHelpPrompts.BOOKSand bindsBookHelpTools(list_folder,search_books,get_book_settings) toBookHelpToolsImploverHelpBookData(aBookDataSource), which runs each call in the request of the asking user, because the functions run on worker threads. The data is limited (100 entries, 20 matches, shortened descriptions), the text of a story is never returned and the prompts are reported only as custom or built-in. See the help assistant. -
Statistics. The rows of the writing statistics hold the folder path of their book, so
FolderServiceImplcallsManuscriptStatisticService.refreshFolders()after a rename, a move, a delete (the content moves up) and every transfer of the Organise window; it reads the placements fromManuscriptRepository.findPlacementsandFolderRepository.findPlacements. -
Cleanup.
Manuscript.folderisWEAKandFolder.parentisOWNED_BY, so a purge of a folder unlinks live books and purges the folders below it.CleanupServiceTesthas the cases. -
Trash. Only books appear in the Trash; a deleted folder does not (its books do, when deleted with it).
Adding to the feature
-
A new inheritable setting: field on
ManuscriptandFolder(extended attributes), a constant ofPromptField, a row inPromptSettingsForm, the setter inFolderService.Detailshandling, the text inLand the bundles. The backup and the export walkPromptField.values(), so they pick it up. -
A setting of a plugin: not a core change. A plugin registers an
InheritedSettingand the dialogs, the resolution (ManuscriptService.resolveExtension) and the backups handle it, see Inherited settings. The values are in theattributesof the book and the folder, not in fields. -
A new list or picker of books: use
BookLabelsfor the names andFolderService.getPathsfor the paths. -
A new action in Organise: a constant in
ShortcutActions, an entry inShortcutRegistry, a button in the row ofOrganiseDialogand the key handling inJavascriptLibrary.fireOrganiseKeys. -
Anything that changes what a book inherits (moving a folder, a setting) must go through
FolderServiceso the agent lock is checked.
Tests
FolderCrudTest (the OwnedCrudContract), FolderServiceTest, FolderTransferTest, FolderListTest, FolderSettingsTest,
SettingsInheritanceTest, ModificationTest (the modification of books and folders), FolderSettingsGenerationTest (the style of a
folder reaches the request the mock server receives), BackupFoldersTest, BackupExportInheritanceTest, AgentFoldersTest,
HelpAssistantBooksTest, CleanupServiceTest (purge cases), and the UI tests FolderDialogTest, OrganiseDialogTest,
OrganiseDialogFlowTest, BookPromptSettingsTest, MassChange*DialogTest, ManuscriptPartFoldersTest,
PhoneManuscriptPartFoldersTest, ViewerFoldersTest (the viewer's list, with a UI that records the cookie writes), BookLabelsTest. See Testing.