Full Code of gristlabs/grist-core for AI

main c2f6e1fc8184 cached
2151 files
19.8 MB
5.3M tokens
15508 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (21,136K chars total). Download the full file to get everything.
Repository: gristlabs/grist-core
Branch: main
Commit: c2f6e1fc8184
Files: 2151
Total size: 19.8 MB

Directory structure:
gitextract_r_s8ij64/

├── .dockerignore
├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 00-bug-issue.yml
│   │   ├── 10-installation-issue.yml
│   │   ├── 20-feature-request.yml
│   │   └── config.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── cla/
│   │   ├── individual-cla.md
│   │   └── signatures.json
│   └── workflows/
│       ├── cla.yml
│       ├── docker.yml
│       ├── docker_latest.yml
│       ├── fly-build.yml
│       ├── fly-cleanup.yml
│       ├── fly-deploy.yml
│       ├── fly-destroy.yml
│       ├── main.yml
│       ├── self-hosted.yml
│       └── translation_keys.yml
├── .gitignore
├── .nvmrc
├── .yarnrc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE.txt
├── NOTICE.txt
├── README.md
├── SECURITY.md
├── app/
│   ├── cli.sh
│   ├── client/
│   │   ├── DefaultHooks.ts
│   │   ├── Hooks.ts
│   │   ├── aclui/
│   │   │   ├── ACLColumnList.ts
│   │   │   ├── ACLFormulaEditor.ts
│   │   │   ├── ACLMemoEditor.ts
│   │   │   ├── ACLSelect.ts
│   │   │   ├── ACLUsers.ts
│   │   │   ├── AccessRules.ts
│   │   │   └── PermissionsWidget.ts
│   │   ├── apiconsole.ts
│   │   ├── app.css
│   │   ├── app.js
│   │   ├── billingMain.ts
│   │   ├── browserCheck.ts
│   │   ├── components/
│   │   │   ├── AceEditor.css
│   │   │   ├── AceEditor.js
│   │   │   ├── AceEditorCompletions.ts
│   │   │   ├── ActionCounter.ts
│   │   │   ├── ActionLog.css
│   │   │   ├── ActionLog.ts
│   │   │   ├── Banner.ts
│   │   │   ├── BaseView.ts
│   │   │   ├── BaseView2.ts
│   │   │   ├── BehavioralPromptsManager.ts
│   │   │   ├── CellPosition.ts
│   │   │   ├── CellSelector.ts
│   │   │   ├── ChartView.css
│   │   │   ├── ChartView.ts
│   │   │   ├── ClientScope.ts
│   │   │   ├── Clipboard.css
│   │   │   ├── Clipboard.ts
│   │   │   ├── CodeEditorPanel.css
│   │   │   ├── CodeEditorPanel.ts
│   │   │   ├── ColumnFilters.css
│   │   │   ├── ColumnTransform.ts
│   │   │   ├── Comm.ts
│   │   │   ├── CopySelection.ts
│   │   │   ├── CoreBanners.ts
│   │   │   ├── Cursor.ts
│   │   │   ├── CursorMonitor.ts
│   │   │   ├── CustomCalendarView.ts
│   │   │   ├── CustomView.css
│   │   │   ├── CustomView.ts
│   │   │   ├── DataTables.ts
│   │   │   ├── DetailView.css
│   │   │   ├── DetailView.ts
│   │   │   ├── DocComm.ts
│   │   │   ├── DocumentUsage.ts
│   │   │   ├── Drafts.ts
│   │   │   ├── DropdownConditionConfig.ts
│   │   │   ├── DropdownConditionEditor.ts
│   │   │   ├── EditorMonitor.ts
│   │   │   ├── EmbedForm.css
│   │   │   ├── ExternalAttachmentBanner.ts
│   │   │   ├── FieldConfigTab.css
│   │   │   ├── FormRenderer.ts
│   │   │   ├── FormRendererCss.ts
│   │   │   ├── Forms/
│   │   │   │   ├── Columns.ts
│   │   │   │   ├── Editor.ts
│   │   │   │   ├── Field.ts
│   │   │   │   ├── FormConfig.ts
│   │   │   │   ├── FormView.ts
│   │   │   │   ├── MappedFieldsConfig.ts
│   │   │   │   ├── Menu.ts
│   │   │   │   ├── Model.ts
│   │   │   │   ├── Paragraph.ts
│   │   │   │   ├── Section.ts
│   │   │   │   ├── Submit.ts
│   │   │   │   ├── elements.ts
│   │   │   │   └── styles.ts
│   │   │   ├── FormulaTransform.ts
│   │   │   ├── GridView.css
│   │   │   ├── GridView.ts
│   │   │   ├── GristClientSocket.ts
│   │   │   ├── GristDoc.css
│   │   │   ├── GristDoc.ts
│   │   │   ├── GristWSConnection.ts
│   │   │   ├── Importer.ts
│   │   │   ├── KeyboardFocusHighlighter.ts
│   │   │   ├── Layout.css
│   │   │   ├── Layout.ts
│   │   │   ├── LayoutEditor.css
│   │   │   ├── LayoutEditor.ts
│   │   │   ├── LayoutTray.ts
│   │   │   ├── LinkingState.ts
│   │   │   ├── Login.css
│   │   │   ├── ParseOptions.ts
│   │   │   ├── PluginScreen.ts
│   │   │   ├── Printing.css
│   │   │   ├── Printing.ts
│   │   │   ├── RawDataPage.ts
│   │   │   ├── RecordCardPopup.ts
│   │   │   ├── RecordLayout.css
│   │   │   ├── RecordLayout.js
│   │   │   ├── RecordLayoutEditor.js
│   │   │   ├── RefSelect.ts
│   │   │   ├── RegionFocusSwitcher.ts
│   │   │   ├── SearchBar.css
│   │   │   ├── SelectionSummary.ts
│   │   │   ├── TypeConversion.ts
│   │   │   ├── TypeTransform.ts
│   │   │   ├── UndoStack.ts
│   │   │   ├── UnsavedChanges.ts
│   │   │   ├── VersionUpdateBanner.ts
│   │   │   ├── ViewAsBanner.ts
│   │   │   ├── ViewConfigTab.css
│   │   │   ├── ViewConfigTab.js
│   │   │   ├── ViewLayout.css
│   │   │   ├── ViewLayout.ts
│   │   │   ├── ViewLinker.css
│   │   │   ├── ViewPane.ts
│   │   │   ├── VirtualDoc.ts
│   │   │   ├── VirtualTable.ts
│   │   │   ├── WidgetFrame.ts
│   │   │   ├── buildViewSectionDom.ts
│   │   │   ├── commandList.ts
│   │   │   ├── commands.css
│   │   │   ├── commands.ts
│   │   │   ├── duplicatePage.ts
│   │   │   ├── duplicateWidget.ts
│   │   │   ├── modals.ts
│   │   │   ├── viewCommon.css
│   │   │   └── viewCommon.js
│   │   ├── declarations.d.ts
│   │   ├── errorMain.ts
│   │   ├── exposeModulesForTests.js
│   │   ├── formMain.ts
│   │   ├── lib/
│   │   │   ├── ACIndex.ts
│   │   │   ├── ACSelect.ts
│   │   │   ├── ACUserManager.ts
│   │   │   ├── BoxSpec.ts
│   │   │   ├── CellDiffTool.ts
│   │   │   ├── CustomSectionElement.ts
│   │   │   ├── Delay.ts
│   │   │   ├── DocPluginManager.ts
│   │   │   ├── DocSchemaImport.ts
│   │   │   ├── FocusLayer.ts
│   │   │   ├── GristWindow.ts
│   │   │   ├── HomePluginManager.ts
│   │   │   ├── ImportSourceElement.ts
│   │   │   ├── Mousetrap.js
│   │   │   ├── MultiUserManager.ts
│   │   │   ├── ObservableMap.js
│   │   │   ├── ObservableSet.js
│   │   │   ├── ReferenceUtils.ts
│   │   │   ├── SafeBrowser.ts
│   │   │   ├── SafeBrowserProcess.css
│   │   │   ├── Signal.ts
│   │   │   ├── Suggestions.ts
│   │   │   ├── TokenField.ts
│   │   │   ├── UrlState.ts
│   │   │   ├── Validator.ts
│   │   │   ├── airtable/
│   │   │   │   ├── AirtableImportUI.ts
│   │   │   │   ├── AirtableImporter.ts
│   │   │   │   ├── startDocAirtableImport.ts
│   │   │   │   └── startHomeAirtableImport.ts
│   │   │   ├── autocomplete.ts
│   │   │   ├── browserGlobals.ts
│   │   │   ├── browserInfo.ts
│   │   │   ├── chartUtil.ts
│   │   │   ├── clipboardUtils.ts
│   │   │   ├── dblclick.ts
│   │   │   ├── dispose.d.ts
│   │   │   ├── dispose.js
│   │   │   ├── dom.js
│   │   │   ├── domAsync.ts
│   │   │   ├── domUtils.ts
│   │   │   ├── download.js
│   │   │   ├── formUtils.ts
│   │   │   ├── formatUtils.ts
│   │   │   ├── fromKoSave.ts
│   │   │   ├── getOrCreateStyleElement.ts
│   │   │   ├── guessTimezone.ts
│   │   │   ├── hashUtils.ts
│   │   │   ├── helpScout.ts
│   │   │   ├── imports.d.ts
│   │   │   ├── imports.js
│   │   │   ├── isFocusable.ts
│   │   │   ├── koArray.d.ts
│   │   │   ├── koArray.js
│   │   │   ├── koArrayWrap.ts
│   │   │   ├── koDom.js
│   │   │   ├── koDomScrolly.css
│   │   │   ├── koDomScrolly.js
│   │   │   ├── koForm.css
│   │   │   ├── koForm.js
│   │   │   ├── koUtil.js
│   │   │   ├── loadScript.ts
│   │   │   ├── localStorageObs.ts
│   │   │   ├── localization.ts
│   │   │   ├── log.ts
│   │   │   ├── markdown.ts
│   │   │   ├── nameUtils.ts
│   │   │   ├── pausableObs.ts
│   │   │   ├── popupControl.ts
│   │   │   ├── popupUtils.ts
│   │   │   ├── sanitizeUrl.ts
│   │   │   ├── sessionObs.ts
│   │   │   ├── simpleList.ts
│   │   │   ├── sortUtil.ts
│   │   │   ├── storage.ts
│   │   │   ├── tableUtil.ts
│   │   │   ├── telemetry.ts
│   │   │   ├── testState.ts
│   │   │   ├── textUtils.ts
│   │   │   ├── timeUtils.ts
│   │   │   ├── trapTabKey.ts
│   │   │   ├── uploads.ts
│   │   │   └── urlUtils.ts
│   │   ├── logo.css
│   │   ├── models/
│   │   │   ├── AdminChecks.ts
│   │   │   ├── AppModel.ts
│   │   │   ├── AuditLogsModel.ts
│   │   │   ├── BaseRowModel.js
│   │   │   ├── ChatHistory.ts
│   │   │   ├── ClientColumnGetters.ts
│   │   │   ├── ColumnACIndexes.ts
│   │   │   ├── ColumnCache.ts
│   │   │   ├── ColumnFilter.ts
│   │   │   ├── ColumnFilterMenuModel.ts
│   │   │   ├── ColumnToMap.ts
│   │   │   ├── ConnectState.ts
│   │   │   ├── DataRowModel.ts
│   │   │   ├── DataTableModel.js
│   │   │   ├── DataTableModelWithDiff.ts
│   │   │   ├── DocData.ts
│   │   │   ├── DocModel.ts
│   │   │   ├── DocPageModel.ts
│   │   │   ├── FormModel.ts
│   │   │   ├── HomeModel.ts
│   │   │   ├── MetaRowModel.js
│   │   │   ├── MetaTableModel.js
│   │   │   ├── NotifyModel.ts
│   │   │   ├── QuerySet.ts
│   │   │   ├── RuleOwner.ts
│   │   │   ├── SearchModel.ts
│   │   │   ├── SectionFilter.ts
│   │   │   ├── Styles.ts
│   │   │   ├── TableData.ts
│   │   │   ├── TableModel.js
│   │   │   ├── TelemetryModel.ts
│   │   │   ├── TimeQuery.ts
│   │   │   ├── ToggleEnterpriseModel.ts
│   │   │   ├── TreeModel.ts
│   │   │   ├── UnionRowSource.ts
│   │   │   ├── UserManagerModel.ts
│   │   │   ├── UserPrefs.ts
│   │   │   ├── UserPresenceModel.ts
│   │   │   ├── ViewFieldConfig.ts
│   │   │   ├── VirtualTable.ts
│   │   │   ├── VirtualTableMeta.ts
│   │   │   ├── WorkspaceInfo.ts
│   │   │   ├── entities/
│   │   │   │   ├── ACLRuleRec.ts
│   │   │   │   ├── CellRec.ts
│   │   │   │   ├── ColumnRec.ts
│   │   │   │   ├── DocInfoRec.ts
│   │   │   │   ├── FilterRec.ts
│   │   │   │   ├── PageRec.ts
│   │   │   │   ├── ShareRec.ts
│   │   │   │   ├── TabBarRec.ts
│   │   │   │   ├── TableRec.ts
│   │   │   │   ├── ValidationRec.ts
│   │   │   │   ├── ViewFieldRec.ts
│   │   │   │   ├── ViewRec.ts
│   │   │   │   └── ViewSectionRec.ts
│   │   │   ├── errors.ts
│   │   │   ├── features.ts
│   │   │   ├── gristConfigCache.ts
│   │   │   ├── gristUrlState.ts
│   │   │   ├── homeUrl.ts
│   │   │   ├── modelUtil.js
│   │   │   ├── rowset.ts
│   │   │   └── rowuid.js
│   │   ├── tsconfig.json
│   │   ├── ui/
│   │   │   ├── AccountPage.ts
│   │   │   ├── AccountPageCss.ts
│   │   │   ├── AccountWidget.ts
│   │   │   ├── AccountWidgetCss.ts
│   │   │   ├── ActiveUserList.ts
│   │   │   ├── AddNewButton.ts
│   │   │   ├── AddNewTip.ts
│   │   │   ├── AdminLeftPanel.ts
│   │   │   ├── AdminPanel.ts
│   │   │   ├── AdminPanelCss.ts
│   │   │   ├── AdminPanelName.ts
│   │   │   ├── AdminTogglesCss.ts
│   │   │   ├── ApiKey.ts
│   │   │   ├── App.css
│   │   │   ├── App.ts
│   │   │   ├── AppHeader.ts
│   │   │   ├── AppUI.ts
│   │   │   ├── AuditLogStreamingConfig.ts
│   │   │   ├── AuditLogsPage.ts
│   │   │   ├── AuthenticationSection.ts
│   │   │   ├── BottomBar.ts
│   │   │   ├── CardContextMenu.ts
│   │   │   ├── CellContextMenu.ts
│   │   │   ├── ChangeAdminModal.ts
│   │   │   ├── CodeHighlight.ts
│   │   │   ├── ColumnFilterCalendarView.ts
│   │   │   ├── ColumnFilterMenu.ts
│   │   │   ├── ColumnFilterMenuUtils.ts
│   │   │   ├── ColumnTitle.ts
│   │   │   ├── ConfigsAPI.ts
│   │   │   ├── CoreHomeImports.ts
│   │   │   ├── CoreNewDocMethods.ts
│   │   │   ├── CreateTeamModal.ts
│   │   │   ├── CustomSectionConfig.ts
│   │   │   ├── CustomThemes.ts
│   │   │   ├── CustomWidgetGallery.ts
│   │   │   ├── DateRangeOptions.ts
│   │   │   ├── DefaultActivationPage.ts
│   │   │   ├── DescriptionConfig.ts
│   │   │   ├── DocHistory.ts
│   │   │   ├── DocIcon.ts
│   │   │   ├── DocList.ts
│   │   │   ├── DocMenu.ts
│   │   │   ├── DocMenuCss.ts
│   │   │   ├── DocTour.ts
│   │   │   ├── DocTutorial.css
│   │   │   ├── DocTutorial.ts
│   │   │   ├── DocTutorialRenderer.ts
│   │   │   ├── DocumentSettings.ts
│   │   │   ├── DuplicateTable.ts
│   │   │   ├── EmojiPicker.ts
│   │   │   ├── ExampleCard.ts
│   │   │   ├── ExampleInfo.ts
│   │   │   ├── Experiments.ts
│   │   │   ├── FieldConfig.ts
│   │   │   ├── FieldContextMenu.ts
│   │   │   ├── FieldMenus.ts
│   │   │   ├── FileDialog.ts
│   │   │   ├── FilterBar.ts
│   │   │   ├── FilterConfig.ts
│   │   │   ├── FloatingPopup.ts
│   │   │   ├── FormAPI.ts
│   │   │   ├── FormContainer.ts
│   │   │   ├── FormErrorPage.ts
│   │   │   ├── FormPage.ts
│   │   │   ├── FormSuccessPage.ts
│   │   │   ├── GetGristComProvider.ts
│   │   │   ├── GridOptions.ts
│   │   │   ├── GridViewMenus.ts
│   │   │   ├── GridViewMenusDateHelpers.ts
│   │   │   ├── GristTooltips.ts
│   │   │   ├── HomeIntro.ts
│   │   │   ├── HomeIntroCards.ts
│   │   │   ├── HomeLeftPane.ts
│   │   │   ├── IAssistantPopup.ts
│   │   │   ├── ImportProgress.ts
│   │   │   ├── LanguageMenu.ts
│   │   │   ├── LeftPanelCommon.ts
│   │   │   ├── LinkConfig.ts
│   │   │   ├── LoginPagesCss.ts
│   │   │   ├── MakeCopyMenu.ts
│   │   │   ├── MarkdownCellRenderer.ts
│   │   │   ├── MenuToggle.ts
│   │   │   ├── MultiSelector.ts
│   │   │   ├── NewRecordButton.ts
│   │   │   ├── NotifyUI.ts
│   │   │   ├── OnBoardingPopups.ts
│   │   │   ├── OnboardingPage.ts
│   │   │   ├── OpenAccessibilityModal.ts
│   │   │   ├── OpenUserManager.ts
│   │   │   ├── OpenVideoTour.ts
│   │   │   ├── PagePanels.ts
│   │   │   ├── PageWidgetPicker.ts
│   │   │   ├── Pages.ts
│   │   │   ├── PinnedDocs.ts
│   │   │   ├── PredefinedCustomSectionConfig.ts
│   │   │   ├── ProposedChangesPage.ts
│   │   │   ├── RelativeDatesOptions.ts
│   │   │   ├── RenameDocModal.ts
│   │   │   ├── RenamePopupStyles.ts
│   │   │   ├── RightPanel.ts
│   │   │   ├── RightPanelStyles.ts
│   │   │   ├── RightPanelUtils.ts
│   │   │   ├── RowContextMenu.ts
│   │   │   ├── RowHeightConfig.ts
│   │   │   ├── ShareMenu.ts
│   │   │   ├── ShortcutKey.ts
│   │   │   ├── SiteSwitcher.ts
│   │   │   ├── SortConfig.ts
│   │   │   ├── SortFilterConfig.ts
│   │   │   ├── SupportGristButton.ts
│   │   │   ├── SupportGristPage.ts
│   │   │   ├── TemplateDocs.ts
│   │   │   ├── ThemeConfig.ts
│   │   │   ├── TimingPage.ts
│   │   │   ├── ToggleEnterpriseWidget.ts
│   │   │   ├── Tools.ts
│   │   │   ├── TopBar.ts
│   │   │   ├── TopBarCss.ts
│   │   │   ├── TreeViewComponent.ts
│   │   │   ├── TreeViewComponentCss.ts
│   │   │   ├── TriggerFormulas.ts
│   │   │   ├── UserImage.ts
│   │   │   ├── UserItem.ts
│   │   │   ├── UserManager.ts
│   │   │   ├── ViewLayoutMenu.ts
│   │   │   ├── ViewSectionMenu.ts
│   │   │   ├── VisibleFieldsConfig.ts
│   │   │   ├── WebhookPage.ts
│   │   │   ├── WelcomeCoachingCall.ts
│   │   │   ├── WelcomePage.ts
│   │   │   ├── WelcomeSitePicker.ts
│   │   │   ├── WelcomeTour.ts
│   │   │   ├── WidgetTitle.ts
│   │   │   ├── YouTubePlayer.ts
│   │   │   ├── buildReassignModal.ts
│   │   │   ├── buttons.ts
│   │   │   ├── contextMenu.ts
│   │   │   ├── createAppPage.ts
│   │   │   ├── createPage.ts
│   │   │   ├── cssInput.ts
│   │   │   ├── errorPages.ts
│   │   │   ├── forms.ts
│   │   │   ├── googleAuth.ts
│   │   │   ├── inputs.ts
│   │   │   ├── mouseDrag.ts
│   │   │   ├── resizeHandle.ts
│   │   │   ├── sanitizeHTML.ts
│   │   │   ├── searchDropdown.ts
│   │   │   ├── selectBy.ts
│   │   │   ├── sendToDrive.ts
│   │   │   ├── shadowScroll.ts
│   │   │   ├── tooltips.ts
│   │   │   ├── transientInput.ts
│   │   │   ├── transitions.ts
│   │   │   ├── userTrustsCustomWidget.ts
│   │   │   ├── viewport.ts
│   │   │   └── widgetTypesMap.ts
│   │   ├── ui2018/
│   │   │   ├── ColorPalette.ts
│   │   │   ├── ColorSelect.ts
│   │   │   ├── IconList.ts
│   │   │   ├── alerts.ts
│   │   │   ├── ariaTabs.ts
│   │   │   ├── breadcrumbs.ts
│   │   │   ├── buttonSelect.ts
│   │   │   ├── buttons.ts
│   │   │   ├── checkbox.ts
│   │   │   ├── cssVars.ts
│   │   │   ├── draggableList.ts
│   │   │   ├── editableLabel.ts
│   │   │   ├── icons.ts
│   │   │   ├── links.ts
│   │   │   ├── loaders.ts
│   │   │   ├── menus.ts
│   │   │   ├── modals.ts
│   │   │   ├── pages.ts
│   │   │   ├── popups.ts
│   │   │   ├── radio.ts
│   │   │   ├── search.ts
│   │   │   ├── select.ts
│   │   │   ├── stretchedLink.ts
│   │   │   ├── tabs.ts
│   │   │   ├── theme.ts
│   │   │   ├── toggleSwitch.ts
│   │   │   ├── unstyled.ts
│   │   │   └── visuallyHidden.ts
│   │   └── widgets/
│   │       ├── AbstractWidget.js
│   │       ├── Assistant.ts
│   │       ├── AttachmentsEditor.ts
│   │       ├── AttachmentsWidget.ts
│   │       ├── BaseEditor.js
│   │       ├── CellStyle.ts
│   │       ├── CheckBox.css
│   │       ├── CheckBoxEditor.js
│   │       ├── ChoiceEditor.js
│   │       ├── ChoiceListCell.ts
│   │       ├── ChoiceListEditor.ts
│   │       ├── ChoiceListEntry.ts
│   │       ├── ChoiceTextBox.ts
│   │       ├── ChoiceToken.ts
│   │       ├── ConditionalStyle.ts
│   │       ├── CurrencyPicker.ts
│   │       ├── DateEditor.ts
│   │       ├── DateTextBox.js
│   │       ├── DateTimeEditor.css
│   │       ├── DateTimeEditor.ts
│   │       ├── DateTimeTextBox.js
│   │       ├── DiffBox.ts
│   │       ├── DiscussionEditor.ts
│   │       ├── EditorButtons.ts
│   │       ├── EditorPlacement.ts
│   │       ├── EditorTooltip.ts
│   │       ├── ErrorDom.ts
│   │       ├── FieldBuilder.css
│   │       ├── FieldBuilder.ts
│   │       ├── FieldEditor.ts
│   │       ├── FloatingEditor.ts
│   │       ├── FormulaAssistant.ts
│   │       ├── FormulaEditor.ts
│   │       ├── HyperLinkEditor.ts
│   │       ├── HyperLinkTextBox.ts
│   │       ├── MarkdownTextBox.ts
│   │       ├── MentionTextBox.ts
│   │       ├── NTextBox.ts
│   │       ├── NTextEditor.ts
│   │       ├── NewAbstractWidget.ts
│   │       ├── NewBaseEditor.ts
│   │       ├── NumericEditor.ts
│   │       ├── NumericSpinner.ts
│   │       ├── NumericTextBox.ts
│   │       ├── Reference.css
│   │       ├── Reference.ts
│   │       ├── ReferenceEditor.ts
│   │       ├── ReferenceList.ts
│   │       ├── ReferenceListEditor.ts
│   │       ├── ReverseReferenceConfig.ts
│   │       ├── Spinner.css
│   │       ├── Spinner.ts
│   │       ├── TZAutocomplete.ts
│   │       ├── TextBox.css
│   │       ├── TextEditor.css
│   │       ├── TextEditor.js
│   │       ├── Toggle.ts
│   │       ├── UserType.ts
│   │       └── UserTypeImpl.ts
│   ├── common/
│   │   ├── ACLPermissions.ts
│   │   ├── ACLRuleCollection.ts
│   │   ├── ACLRulesReader.ts
│   │   ├── ActionBundle.ts
│   │   ├── ActionDispatcher.ts
│   │   ├── ActionGroup.ts
│   │   ├── ActionRouter.ts
│   │   ├── ActionSummarizer.ts
│   │   ├── ActionSummary.ts
│   │   ├── ActivationAPI.ts
│   │   ├── ActiveDocAPI.ts
│   │   ├── AlternateActions.ts
│   │   ├── ApiError.ts
│   │   ├── Assistance.ts
│   │   ├── Assistant.ts
│   │   ├── AsyncCreate.ts
│   │   ├── AsyncFlow.ts
│   │   ├── AttachmentColumns.ts
│   │   ├── BaseAPI.ts
│   │   ├── BasketClientAPI.ts
│   │   ├── BigInt.ts
│   │   ├── BillingAPI.ts
│   │   ├── BinaryIndexedTree.js
│   │   ├── BootProbe.ts
│   │   ├── BrowserSettings.ts
│   │   ├── CircularArray.js
│   │   ├── ColumnFilterFunc.ts
│   │   ├── ColumnGetters.ts
│   │   ├── CommTypes.ts
│   │   ├── Config-ti.ts
│   │   ├── Config.ts
│   │   ├── ConfigAPI.ts
│   │   ├── CssCustomProp.ts
│   │   ├── CustomWidget.ts
│   │   ├── DisposableWithEvents.ts
│   │   ├── DocActions.ts
│   │   ├── DocComments.ts
│   │   ├── DocData.ts
│   │   ├── DocDataCache.ts
│   │   ├── DocLimits.ts
│   │   ├── DocListAPI.ts
│   │   ├── DocSchemaImport.ts
│   │   ├── DocSchemaImportTypes-ti.ts
│   │   ├── DocSchemaImportTypes.ts
│   │   ├── DocSnapshot.ts
│   │   ├── DocState.ts
│   │   ├── DocUsage.ts
│   │   ├── DocumentSettings-ti.ts
│   │   ├── DocumentSettings.ts
│   │   ├── DropdownCondition.ts
│   │   ├── EncActionBundle.ts
│   │   ├── ErrorWithCode.ts
│   │   ├── Features-ti.ts
│   │   ├── Features.ts
│   │   ├── FilterState.ts
│   │   ├── Forms.ts
│   │   ├── Formula.ts
│   │   ├── GranularAccessClause.ts
│   │   ├── GristServerAPI.ts
│   │   ├── ICommonUrls-ti.ts
│   │   ├── ICommonUrls.ts
│   │   ├── InactivityTimer.ts
│   │   ├── Install.ts
│   │   ├── InstallAPI.ts
│   │   ├── Interval.ts
│   │   ├── KeyedMutex.ts
│   │   ├── KeyedOps.ts
│   │   ├── Limits.ts
│   │   ├── LinkNode.ts
│   │   ├── LocaleCodes.ts
│   │   ├── Locales.ts
│   │   ├── LoginSessionAPI.ts
│   │   ├── MemBuffer.js
│   │   ├── NumberFormat.ts
│   │   ├── NumberParse.ts
│   │   ├── PluginInstance.ts
│   │   ├── PredicateFormula.ts
│   │   ├── Prefs.ts
│   │   ├── RecentItems.js
│   │   ├── RecordView.ts
│   │   ├── RefCountMap.ts
│   │   ├── RelativeDates.ts
│   │   ├── RowFilterFunc.ts
│   │   ├── SandboxInfo.ts
│   │   ├── ServiceAccountTypes-ti.ts
│   │   ├── ServiceAccountTypes.ts
│   │   ├── ShareAnnotator.ts
│   │   ├── ShareOptions.ts
│   │   ├── SortFunc.ts
│   │   ├── SortSpec.ts
│   │   ├── StringUnion.ts
│   │   ├── TableData.ts
│   │   ├── TabularDiff.ts
│   │   ├── Telemetry.ts
│   │   ├── TestState.ts
│   │   ├── ThemePrefs.ts
│   │   ├── Themes.ts
│   │   ├── TimeQuery.ts
│   │   ├── Triggers-ti.ts
│   │   ├── Triggers.ts
│   │   ├── User.ts
│   │   ├── UserAPI.ts
│   │   ├── UserConfig.ts
│   │   ├── ValueConverter.ts
│   │   ├── ValueFormatter.ts
│   │   ├── ValueGuesser.ts
│   │   ├── ValueParser.ts
│   │   ├── WidgetOptions.ts
│   │   ├── airtable/
│   │   │   ├── AirtableAPI.ts
│   │   │   ├── AirtableAPITypes-ti.ts
│   │   │   ├── AirtableAPITypes.ts
│   │   │   ├── AirtableAttachmentTracker.ts
│   │   │   ├── AirtableCrosswalk.ts
│   │   │   ├── AirtableDataImporter.ts
│   │   │   ├── AirtableDataImporterTypes.ts
│   │   │   ├── AirtableReferenceTracker.ts
│   │   │   └── AirtableSchemaImporter.ts
│   │   ├── arrayToString.ts
│   │   ├── asyncIterators.ts
│   │   ├── csvFormat.ts
│   │   ├── declarations.d.ts
│   │   ├── delay.ts
│   │   ├── emails.ts
│   │   ├── getCurrentTime.ts
│   │   ├── gristTypes.ts
│   │   ├── gristUrls.ts
│   │   ├── gutil.ts
│   │   ├── isHiddenTable.ts
│   │   ├── loginProviders.ts
│   │   ├── marshal.ts
│   │   ├── normalizedDateTimeString.ts
│   │   ├── orgNameUtils.ts
│   │   ├── parseDate.ts
│   │   ├── plugin.ts
│   │   ├── resetOrg.ts
│   │   ├── roles.ts
│   │   ├── schema.ts
│   │   ├── tagManager.ts
│   │   ├── tbind.ts
│   │   ├── themes/
│   │   │   ├── Base.ts
│   │   │   ├── GristDark.ts
│   │   │   ├── GristLight.ts
│   │   │   └── HighContrastLight.ts
│   │   ├── timeFormat.ts
│   │   ├── tpromisified.ts
│   │   ├── tsconfig.json
│   │   ├── tsvFormat.ts
│   │   ├── uploads.ts
│   │   ├── urlUtils.ts
│   │   └── widgetTypes.ts
│   ├── gen-server/
│   │   ├── ApiServer.ts
│   │   ├── entity/
│   │   │   ├── AclRule.ts
│   │   │   ├── Activation.ts
│   │   │   ├── Alias.ts
│   │   │   ├── BillingAccount.ts
│   │   │   ├── BillingAccountManager.ts
│   │   │   ├── Config.ts
│   │   │   ├── DocPref.ts
│   │   │   ├── Document.ts
│   │   │   ├── Group.ts
│   │   │   ├── Limit.ts
│   │   │   ├── Login.ts
│   │   │   ├── OAuthClient.ts
│   │   │   ├── OAuthGrant.ts
│   │   │   ├── Organization.ts
│   │   │   ├── Pref.ts
│   │   │   ├── Product.ts
│   │   │   ├── Proposal.ts
│   │   │   ├── Resource.ts
│   │   │   ├── Secret.ts
│   │   │   ├── ServiceAccount.ts
│   │   │   ├── Share.ts
│   │   │   ├── User.ts
│   │   │   └── Workspace.ts
│   │   ├── lib/
│   │   │   ├── ActivationsManager.ts
│   │   │   ├── DocApiForwarder.ts
│   │   │   ├── DocWorkerMap.ts
│   │   │   ├── Doom.ts
│   │   │   ├── Housekeeper.ts
│   │   │   ├── NotifierTypes.ts
│   │   │   ├── Permissions.ts
│   │   │   ├── TypeORMPatches.ts
│   │   │   ├── Usage.ts
│   │   │   ├── homedb/
│   │   │   │   ├── Caches.ts
│   │   │   │   ├── GroupsManager.ts
│   │   │   │   ├── HomeDBManager.ts
│   │   │   │   ├── Interfaces.ts
│   │   │   │   ├── ServiceAccountsManager.ts
│   │   │   │   └── UsersManager.ts
│   │   │   ├── scrubUserFromOrg.ts
│   │   │   └── values.ts
│   │   ├── migration/
│   │   │   ├── 1536634251710-Initial.ts
│   │   │   ├── 1539031763952-Login.ts
│   │   │   ├── 1549313797109-PinDocs.ts
│   │   │   ├── 1549381727494-UserPicture.ts
│   │   │   ├── 1551805156919-LoginDisplayEmail.ts
│   │   │   ├── 1552416614755-LoginDisplayEmailNonNull.ts
│   │   │   ├── 1553016106336-Indexes.ts
│   │   │   ├── 1556726945436-Billing.ts
│   │   │   ├── 1557157922339-OrgDomainUnique.ts
│   │   │   ├── 1561589211752-Aliases.ts
│   │   │   ├── 1568238234987-TeamMembers.ts
│   │   │   ├── 1569593726320-FirstLogin.ts
│   │   │   ├── 1569946508569-FirstTimeUser.ts
│   │   │   ├── 1573569442552-CustomerIndex.ts
│   │   │   ├── 1579559983067-ExtraIndexes.ts
│   │   │   ├── 1591755411755-OrgHost.ts
│   │   │   ├── 1592261300044-DocRemovedAt.ts
│   │   │   ├── 1596456522124-Prefs.ts
│   │   │   ├── 1623871765992-ExternalBilling.ts
│   │   │   ├── 1626369037484-DocOptions.ts
│   │   │   ├── 1631286208009-Secret.ts
│   │   │   ├── 1644363380225-UserOptions.ts
│   │   │   ├── 1647883793388-GracePeriodStart.ts
│   │   │   ├── 1651469582887-DocumentUsage.ts
│   │   │   ├── 1652273656610-Activations.ts
│   │   │   ├── 1652277549983-UserConnectId.ts
│   │   │   ├── 1663851423064-UserUUID.ts
│   │   │   ├── 1664528376930-UserRefUnique.ts
│   │   │   ├── 1673051005072-Forks.ts
│   │   │   ├── 1678737195050-ForkIndexes.ts
│   │   │   ├── 1682636695021-ActivationPrefs.ts
│   │   │   ├── 1685343047786-AssistantLimit.ts
│   │   │   ├── 1701557445716-Shares.ts
│   │   │   ├── 1711557445716-Billing.ts
│   │   │   ├── 1713186031023-UserLastConnection.ts
│   │   │   ├── 1722529827161-Activation-Enabled.ts
│   │   │   ├── 1727747249153-Configs.ts
│   │   │   ├── 1729754662550-LoginsEmailIndex.ts
│   │   │   ├── 1732103776245-GracePeriod.ts
│   │   │   ├── 1738912357827-UserCreatedAt.ts
│   │   │   ├── 1746246433628-DocPref.ts
│   │   │   ├── 1749454162428-GroupUsersCreatedAt.ts
│   │   │   ├── 1753088213255-GroupTypes.ts
│   │   │   ├── 1754077317821-UserDisabledAt.ts
│   │   │   ├── 1756799894986-UserUnsubscribeKey.ts
│   │   │   ├── 1756918816559-ServiceAccounts.ts
│   │   │   ├── 1759256005608-Proposals.ts
│   │   │   ├── 1759434763338-DocDisabledAt.ts
│   │   │   ├── 1764872085347-OAuthClientsAndGrants.ts
│   │   │   └── README.md
│   │   └── sqlUtils.ts
│   ├── plugin/
│   │   ├── CustomSectionAPI-ti.ts
│   │   ├── CustomSectionAPI.ts
│   │   ├── DocApiTypes-ti.ts
│   │   ├── DocApiTypes.ts
│   │   ├── FileParserAPI-ti.ts
│   │   ├── FileParserAPI.ts
│   │   ├── GristAPI-ti.ts
│   │   ├── GristAPI.ts
│   │   ├── GristData-ti.ts
│   │   ├── GristData.ts
│   │   ├── GristTable-ti.ts
│   │   ├── GristTable.ts
│   │   ├── ImportSourceAPI-ti.ts
│   │   ├── ImportSourceAPI.ts
│   │   ├── InternalImportSourceAPI-ti.ts
│   │   ├── InternalImportSourceAPI.ts
│   │   ├── PluginManifest-ti.ts
│   │   ├── PluginManifest.ts
│   │   ├── README.md
│   │   ├── RenderOptions-ti.ts
│   │   ├── RenderOptions.ts
│   │   ├── StorageAPI-ti.ts
│   │   ├── StorageAPI.ts
│   │   ├── TableOperations.ts
│   │   ├── TableOperationsImpl.ts
│   │   ├── TypeCheckers.ts
│   │   ├── WidgetAPI-ti.ts
│   │   ├── WidgetAPI.ts
│   │   ├── grist-plugin-api.ts
│   │   ├── gutil.ts
│   │   ├── objtypes.ts
│   │   └── tsconfig.json
│   ├── server/
│   │   ├── MergedServer.ts
│   │   ├── companion.ts
│   │   ├── declarations.d.ts
│   │   ├── devServerMain.ts
│   │   ├── generateCheckpoint.ts
│   │   ├── generateInitialDocSql.ts
│   │   ├── lib/
│   │   │   ├── AccessTokens.ts
│   │   │   ├── ActionHistory.ts
│   │   │   ├── ActionHistoryImpl.ts
│   │   │   ├── ActiveDoc.ts
│   │   │   ├── ActiveDocImport.ts
│   │   │   ├── ActiveDocUtils.ts
│   │   │   ├── AppEndpoint.ts
│   │   │   ├── AppSettings.ts
│   │   │   ├── Archive.ts
│   │   │   ├── Assistant.ts
│   │   │   ├── AssistantStatePermit.ts
│   │   │   ├── AttachmentFileManager.ts
│   │   │   ├── AttachmentStore.ts
│   │   │   ├── AttachmentStoreProvider.ts
│   │   │   ├── AuditEvent.ts
│   │   │   ├── AuthSession.ts
│   │   │   ├── Authorizer.ts
│   │   │   ├── BootProbes.ts
│   │   │   ├── BrowserSession.ts
│   │   │   ├── CellDataAccess.ts
│   │   │   ├── Client.ts
│   │   │   ├── Comm.ts
│   │   │   ├── ConfigBackendAPI.ts
│   │   │   ├── DiscourseConnect.ts
│   │   │   ├── DocApi.ts
│   │   │   ├── DocApiTriggers.ts
│   │   │   ├── DocApiUtils.ts
│   │   │   ├── DocAuthorizer.ts
│   │   │   ├── DocClients.ts
│   │   │   ├── DocManager.ts
│   │   │   ├── DocPluginData.ts
│   │   │   ├── DocPluginManager.ts
│   │   │   ├── DocSession.ts
│   │   │   ├── DocSnapshots.ts
│   │   │   ├── DocStorage.ts
│   │   │   ├── DocStorageManager.ts
│   │   │   ├── DocWorker.ts
│   │   │   ├── DocWorkerLoadTracker.ts
│   │   │   ├── DocWorkerMap.ts
│   │   │   ├── DocWorkerUtils.ts
│   │   │   ├── ExcelFormatter.ts
│   │   │   ├── ExpandedQuery.ts
│   │   │   ├── Export.ts
│   │   │   ├── ExportDSV.ts
│   │   │   ├── ExportTableSchema.ts
│   │   │   ├── ExportXLSX.ts
│   │   │   ├── ExternalStorage.ts
│   │   │   ├── FileParserElement.ts
│   │   │   ├── FlexServer.ts
│   │   │   ├── ForwardAuthLogin.ts
│   │   │   ├── GetGristComConfig.ts
│   │   │   ├── GoogleAuth.ts
│   │   │   ├── GoogleExport.ts
│   │   │   ├── GoogleImport.ts
│   │   │   ├── GranularAccess.ts
│   │   │   ├── GristJobs.ts
│   │   │   ├── GristServer.ts
│   │   │   ├── GristServerSocket.ts
│   │   │   ├── GristSocketServer.ts
│   │   │   ├── HashUtil.ts
│   │   │   ├── HostedMetadataManager.ts
│   │   │   ├── HostedStorageManager.ts
│   │   │   ├── IAssistant.ts
│   │   │   ├── IAuditLogger.ts
│   │   │   ├── IBilling.ts
│   │   │   ├── IChecksumStore.ts
│   │   │   ├── ICreate.ts
│   │   │   ├── IDocNotificationManager.ts
│   │   │   ├── IDocStorageManager.ts
│   │   │   ├── IElectionStore.ts
│   │   │   ├── INotifier.ts
│   │   │   ├── ISandbox.ts
│   │   │   ├── IShell.ts
│   │   │   ├── ITestingHooks-ti.ts
│   │   │   ├── ITestingHooks.ts
│   │   │   ├── InsightLog.ts
│   │   │   ├── InstallAdmin.ts
│   │   │   ├── LogMethods.ts
│   │   │   ├── LoginSystemConfig.ts
│   │   │   ├── MemoryPool.ts
│   │   │   ├── MinIOExternalStorage.ts
│   │   │   ├── MinimalLogin.ts
│   │   │   ├── NSandbox.ts
│   │   │   ├── NullSandbox.ts
│   │   │   ├── OAuth2Clients.ts
│   │   │   ├── OIDCConfig.ts
│   │   │   ├── OnDemandActions.ts
│   │   │   ├── OpenAIAssistantV1.ts
│   │   │   ├── Patch.ts
│   │   │   ├── PermissionInfo.ts
│   │   │   ├── Permit.ts
│   │   │   ├── PluginEndpoint.ts
│   │   │   ├── PluginManager.ts
│   │   │   ├── ProcessMonitor.ts
│   │   │   ├── ProxyAgent.ts
│   │   │   ├── PubSubCache.ts
│   │   │   ├── PubSubManager.ts
│   │   │   ├── Requests.ts
│   │   │   ├── RowAccess.ts
│   │   │   ├── SQLiteDB.ts
│   │   │   ├── SafePythonComponent.ts
│   │   │   ├── SamlConfig.ts
│   │   │   ├── SandboxControl.ts
│   │   │   ├── SandboxPyodide.ts
│   │   │   ├── ServerColumnGetters.ts
│   │   │   ├── ServerLocale.ts
│   │   │   ├── Sessions.ts
│   │   │   ├── Sharing.ts
│   │   │   ├── SqliteCommon.ts
│   │   │   ├── SqliteNode.ts
│   │   │   ├── TableMetadataLoader.ts
│   │   │   ├── TagChecker.ts
│   │   │   ├── Telemetry.ts
│   │   │   ├── TestLogin.ts
│   │   │   ├── TestingHooks.ts
│   │   │   ├── Throttle.ts
│   │   │   ├── TimeQuery.ts
│   │   │   ├── Triggers.ts
│   │   │   ├── UnsafeNodeComponent.ts
│   │   │   ├── UpdateManager.ts
│   │   │   ├── UserPresence.ts
│   │   │   ├── WebhookQueue.ts
│   │   │   ├── WidgetRepository.ts
│   │   │   ├── attachEarlyEndpoints.ts
│   │   │   ├── backupSqliteDatabase.ts
│   │   │   ├── checksumFile.ts
│   │   │   ├── config.ts
│   │   │   ├── configCore.ts
│   │   │   ├── configCoreFileFormats-ti.ts
│   │   │   ├── configCoreFileFormats.ts
│   │   │   ├── configureMinIOExternalStorage.ts
│   │   │   ├── configureOpenAIAssistantV1.ts
│   │   │   ├── cookieUtils.ts
│   │   │   ├── coreCreator.ts
│   │   │   ├── coreLogins.ts
│   │   │   ├── createSavedDoc.ts
│   │   │   ├── dbUtils.ts
│   │   │   ├── describeDocActions.ts
│   │   │   ├── docUtils.d.ts
│   │   │   ├── docUtils.js
│   │   │   ├── expressWrap.ts
│   │   │   ├── extractOrg.ts
│   │   │   ├── filterUtils.ts
│   │   │   ├── gristSessions.ts
│   │   │   ├── gristSettings.ts
│   │   │   ├── guessExt.ts
│   │   │   ├── hashingUtils.ts
│   │   │   ├── httpEncoding.ts
│   │   │   ├── idUtils.ts
│   │   │   ├── initialDocSql.ts
│   │   │   ├── log.ts
│   │   │   ├── loginSystemHelpers.ts
│   │   │   ├── manifest.ts
│   │   │   ├── middleware.ts
│   │   │   ├── oidc/
│   │   │   │   └── Protections.ts
│   │   │   ├── places.ts
│   │   │   ├── reportTimeTaken.ts
│   │   │   ├── requestUtils.ts
│   │   │   ├── runSQLQuery.ts
│   │   │   ├── sandboxUtil.ts
│   │   │   ├── scim/
│   │   │   │   ├── index.ts
│   │   │   │   └── v2/
│   │   │   │       ├── BaseController.ts
│   │   │   │       ├── ScimGroupController.ts
│   │   │   │       ├── ScimRoleController.ts
│   │   │   │       ├── ScimTypes.ts
│   │   │   │       ├── ScimUserController.ts
│   │   │   │       ├── ScimUtils.ts
│   │   │   │       ├── ScimV2Api.ts
│   │   │   │       └── roles/
│   │   │   │           ├── SCIMMYRoleResource.ts
│   │   │   │           └── SCIMMYRoleSchema.ts
│   │   │   ├── selectBy.ts
│   │   │   ├── sendAppPage.ts
│   │   │   ├── serverUtils.ts
│   │   │   ├── sessionUtils.ts
│   │   │   ├── shortDesc.ts
│   │   │   ├── shutdown.js
│   │   │   ├── updateChecker.ts
│   │   │   ├── uploads.ts
│   │   │   └── workerExporter.ts
│   │   ├── localization.ts
│   │   ├── tsconfig.json
│   │   └── utils/
│   │       ├── LogSanitizer.ts
│   │       ├── gristify.ts
│   │       ├── pruneActionHistory.ts
│   │       ├── showAuditLogEvents.ts
│   │       └── streams.ts
│   └── tsconfig.json
├── buildtools/
│   ├── .grist-ee-version
│   ├── build.sh
│   ├── checkout-ext-directory.sh
│   ├── fly-deploy.js
│   ├── fly-template.env
│   ├── fly-template.toml
│   ├── genIconCSS.ts
│   ├── generate_locale_list.js
│   ├── generate_translation_keys.js
│   ├── install_chrome_for_tests.sh
│   ├── prepare_ee.sh
│   ├── prepare_python.sh
│   ├── sanitize_translations.js
│   ├── tsconfig-base-ext.json
│   ├── tsconfig-base.json
│   ├── update_schema.sh
│   ├── update_type_info.sh
│   ├── webpack.api.config.js
│   ├── webpack.check.js
│   └── webpack.config.js
├── crowdin.yml
├── docker-compose-examples/
│   ├── grist-local-testing/
│   │   ├── README.md
│   │   └── docker-compose.yml
│   ├── grist-traefik-basic-auth/
│   │   ├── README.md
│   │   ├── configs/
│   │   │   ├── traefik-config.yml
│   │   │   └── traefik-dynamic-config.yml
│   │   └── docker-compose.yml
│   ├── grist-traefik-oidc-auth/
│   │   ├── README.md
│   │   ├── configs/
│   │   │   ├── authelia/
│   │   │   │   ├── configuration.yml
│   │   │   │   └── users_database.yml
│   │   │   └── traefik/
│   │   │       └── config.yml
│   │   ├── docker-compose.yml
│   │   ├── env-template
│   │   ├── generateSecureSecrets.sh
│   │   └── secrets_template/
│   │       ├── GRIST_CLIENT_SECRET_DIGEST
│   │       ├── HMAC_SECRET
│   │       ├── JWT_SECRET
│   │       ├── SESSION_SECRET
│   │       ├── STORAGE_ENCRYPTION_KEY
│   │       └── certs/
│   │           └── private.pem
│   ├── grist-with-keycloak-postgres-redis-minio/
│   │   ├── README.md
│   │   └── docker-compose.yml
│   └── grist-with-postgres-redis-minio/
│       ├── README.md
│       └── docker-compose.yml
├── documentation/
│   ├── database.md
│   ├── develop.md
│   ├── disposal.md
│   ├── grainjs.md
│   ├── grist-data-format.md
│   ├── images/
│   │   └── BDD.drawio
│   ├── migrations.md
│   ├── overview.md
│   ├── translations.md
│   └── urls.md
├── eslint.config.js
├── package.json
├── plugins/
│   └── core/
│       └── manifest.yml
├── publiccode.yml
├── sandbox/
│   ├── MANIFEST.in
│   ├── bundle_as_wheel.sh
│   ├── docker/
│   │   ├── Dockerfile
│   │   └── Makefile
│   ├── docker_entrypoint.sh
│   ├── gen_js_schema.py
│   ├── grist/
│   │   ├── acl.py
│   │   ├── action_obj.py
│   │   ├── action_summary.py
│   │   ├── actions.py
│   │   ├── attribute_recorder.py
│   │   ├── autocomplete_context.py
│   │   ├── codebuilder.py
│   │   ├── column.py
│   │   ├── csv_patch.py
│   │   ├── depend.py
│   │   ├── docactions.py
│   │   ├── docmodel.py
│   │   ├── dropdown_condition.py
│   │   ├── engine.py
│   │   ├── fake_std_streams.py
│   │   ├── formula_prompt.py
│   │   ├── friendly_errors.py
│   │   ├── functions/
│   │   │   ├── __init__.py
│   │   │   ├── date.py
│   │   │   ├── info.py
│   │   │   ├── logical.py
│   │   │   ├── lookup.py
│   │   │   ├── math.py
│   │   │   ├── prevnext.py
│   │   │   ├── schedule.py
│   │   │   ├── stats.py
│   │   │   ├── test_schedule.py
│   │   │   ├── text.py
│   │   │   └── unimplemented.py
│   │   ├── gencode.py
│   │   ├── grist.py
│   │   ├── identifiers.py
│   │   ├── import_actions.py
│   │   ├── imports/
│   │   │   ├── __init__.py
│   │   │   ├── fixtures/
│   │   │   │   ├── nyc_schools_progress_report_ec_2013.xlsx
│   │   │   │   ├── strange_dates.xlsx
│   │   │   │   ├── test_boolean.xlsx
│   │   │   │   ├── test_empty_rows.xlsx
│   │   │   │   ├── test_encoding_utf8.csv
│   │   │   │   ├── test_excel.xlsx
│   │   │   │   ├── test_excel_numeric_gs.xlsx
│   │   │   │   ├── test_excel_types.csv
│   │   │   │   ├── test_excel_types.xlsx
│   │   │   │   ├── test_falsy_cells.xlsx
│   │   │   │   ├── test_headers_with_none_cell.xlsx
│   │   │   │   ├── test_import_csv.csv
│   │   │   │   ├── test_invalid_dimensions.xlsx
│   │   │   │   ├── test_isdigit.csv
│   │   │   │   ├── test_long_cell.csv
│   │   │   │   └── test_single_merged_cell.xlsx
│   │   │   ├── import_csv.py
│   │   │   ├── import_csv_test.py
│   │   │   ├── import_json.py
│   │   │   ├── import_json_test.py
│   │   │   ├── import_utils.py
│   │   │   ├── import_xls.py
│   │   │   ├── import_xls_test.py
│   │   │   ├── register.py
│   │   │   └── test_imports.py
│   │   ├── lookup.py
│   │   ├── main.py
│   │   ├── match_counter.py
│   │   ├── migrations.py
│   │   ├── moment.py
│   │   ├── objtypes.py
│   │   ├── parse_data.py
│   │   ├── predicate_formula.py
│   │   ├── records.py
│   │   ├── relabeling.py
│   │   ├── relation.py
│   │   ├── reverse_references.py
│   │   ├── runtests.py
│   │   ├── sandbox.py
│   │   ├── schema.py
│   │   ├── sort_key.py
│   │   ├── sort_specs.py
│   │   ├── summary.py
│   │   ├── table.py
│   │   ├── table_data_set.py
│   │   ├── test_acl_formula.py
│   │   ├── test_acl_renames.py
│   │   ├── test_actions.py
│   │   ├── test_codebuilder.py
│   │   ├── test_column_actions.py
│   │   ├── test_completion.py
│   │   ├── test_date_types.py
│   │   ├── test_default_formulas.py
│   │   ├── test_depend.py
│   │   ├── test_derived.py
│   │   ├── test_display_cols.py
│   │   ├── test_docmodel.py
│   │   ├── test_dropdown_condition.py
│   │   ├── test_dropdown_condition_renames.py
│   │   ├── test_engine.py
│   │   ├── test_find_col.py
│   │   ├── test_formula_error.py
│   │   ├── test_formula_prompt.py
│   │   ├── test_formula_undo.py
│   │   ├── test_functions.py
│   │   ├── test_gencode.py
│   │   ├── test_import_actions.py
│   │   ├── test_lookup_find.py
│   │   ├── test_lookup_perf.py
│   │   ├── test_lookup_sort.py
│   │   ├── test_lookups.py
│   │   ├── test_match_counter.py
│   │   ├── test_migrations.py
│   │   ├── test_moment.py
│   │   ├── test_objtypes.py
│   │   ├── test_predicate_formula.py
│   │   ├── test_prevnext.py
│   │   ├── test_record_func.py
│   │   ├── test_recordlist.py
│   │   ├── test_reflist_rel.py
│   │   ├── test_relabeling.py
│   │   ├── test_renames.py
│   │   ├── test_renames2.py
│   │   ├── test_replace_table_data.py
│   │   ├── test_replay.py
│   │   ├── test_requests.py
│   │   ├── test_rules.py
│   │   ├── test_rules_grid.py
│   │   ├── test_side_effects.py
│   │   ├── test_sort_key.py
│   │   ├── test_sort_spec.py
│   │   ├── test_summary.py
│   │   ├── test_summary2.py
│   │   ├── test_summary_choicelist.py
│   │   ├── test_summary_undo.py
│   │   ├── test_table_actions.py
│   │   ├── test_table_data_set.py
│   │   ├── test_temp_rowids.py
│   │   ├── test_textbuilder.py
│   │   ├── test_treeview.py
│   │   ├── test_trigger_expression.py
│   │   ├── test_trigger_formulas.py
│   │   ├── test_twoway_refs.py
│   │   ├── test_twowaymap.py
│   │   ├── test_types.py
│   │   ├── test_undo.py
│   │   ├── test_urllib_patch.py
│   │   ├── test_user.py
│   │   ├── test_useractions.py
│   │   ├── testsamples.py
│   │   ├── testscript.json
│   │   ├── testutil.py
│   │   ├── textbuilder.py
│   │   ├── timing.py
│   │   ├── treeview.py
│   │   ├── trigger_expression.py
│   │   ├── twowaymap.py
│   │   ├── tzdata.data
│   │   ├── urllib_patch.py
│   │   ├── user.py
│   │   ├── useractions.py
│   │   ├── usercode.py
│   │   ├── usertypes.py
│   │   └── xmlrunner.py
│   ├── gvisor/
│   │   ├── get_checkpoint_path.sh
│   │   ├── run.py
│   │   └── update_engine_checkpoint.sh
│   ├── install_tz.js
│   ├── pyodide/
│   │   ├── Makefile
│   │   ├── README.md
│   │   ├── build_packages.sh
│   │   ├── package.json
│   │   ├── package_filenames.json
│   │   ├── packages.js
│   │   ├── pipe.js
│   │   ├── preparePackages.js
│   │   └── setup.sh
│   ├── requirements.in
│   ├── requirements.txt
│   ├── run.sh
│   ├── setup.py
│   ├── supervisor.mjs
│   └── watch.sh
├── static/
│   ├── apiconsole.html
│   ├── app.html
│   ├── custom-widget.html
│   ├── custom.css
│   ├── error.html
│   ├── form.html
│   ├── icons/
│   │   ├── grist.icns
│   │   ├── gristdoc.icns
│   │   ├── icons.css
│   │   └── locales/
│   │       └── LICENSE
│   ├── index.html
│   ├── locales/
│   │   ├── ar.client.json
│   │   ├── ar.server.json
│   │   ├── bci.client.json
│   │   ├── bci.server.json
│   │   ├── bg.client.json
│   │   ├── bg.server.json
│   │   ├── ca.client.json
│   │   ├── ca.server.json
│   │   ├── cs.client.json
│   │   ├── cs.server.json
│   │   ├── de.client.json
│   │   ├── de.server.json
│   │   ├── el.client.json
│   │   ├── el.server.json
│   │   ├── en.client.json
│   │   ├── en.server.json
│   │   ├── en_GB.client.json
│   │   ├── en_GB.server.json
│   │   ├── es.client.json
│   │   ├── es.server.json
│   │   ├── eu.client.json
│   │   ├── eu.server.json
│   │   ├── fa.client.json
│   │   ├── fa.server.json
│   │   ├── fi.client.json
│   │   ├── fi.server.json
│   │   ├── fr.client.json
│   │   ├── fr.server.json
│   │   ├── hu.client.json
│   │   ├── hu.server.json
│   │   ├── id.client.json
│   │   ├── id.server.json
│   │   ├── ig.client.json
│   │   ├── ig.server.json
│   │   ├── it.client.json
│   │   ├── it.server.json
│   │   ├── ja.client.json
│   │   ├── ja.server.json
│   │   ├── ko.client.json
│   │   ├── ko.server.json
│   │   ├── nb_NO.client.json
│   │   ├── nb_NO.server.json
│   │   ├── nl.client.json
│   │   ├── nl.server.json
│   │   ├── pl.client.json
│   │   ├── pl.server.json
│   │   ├── pt.client.json
│   │   ├── pt.server.json
│   │   ├── pt_BR.client.json
│   │   ├── pt_BR.server.json
│   │   ├── ro.client.json
│   │   ├── ro.server.json
│   │   ├── ru.client.json
│   │   ├── ru.server.json
│   │   ├── sk.client.json
│   │   ├── sk.server.json
│   │   ├── sl.client.json
│   │   ├── sl.server.json
│   │   ├── sv.client.json
│   │   ├── sv.server.json
│   │   ├── ta.client.json
│   │   ├── ta.server.json
│   │   ├── th.client.json
│   │   ├── th.server.json
│   │   ├── tr.client.json
│   │   ├── tr.server.json
│   │   ├── uk.client.json
│   │   ├── uk.server.json
│   │   ├── ur.client.json
│   │   ├── ur.server.json
│   │   ├── vi.client.json
│   │   ├── vi.server.json
│   │   ├── zh_Hans.client.json
│   │   ├── zh_Hans.server.json
│   │   ├── zh_Hant.client.json
│   │   ├── zh_Hant.server.json
│   │   ├── zun.client.json
│   │   └── zun.server.json
│   ├── message.html
│   ├── swagger-ui-dark.css
│   ├── test.html
│   └── testWebdriverJQuery.html
├── stubs/
│   └── app/
│       ├── client/
│       │   ├── components/
│       │   │   └── Banners.ts
│       │   ├── ui/
│       │   │   ├── ActivationPage.ts
│       │   │   ├── AdminControls.ts
│       │   │   ├── BillingPage.ts
│       │   │   ├── ChangePasswordDialog.ts
│       │   │   ├── CustomThemes.ts
│       │   │   ├── DeleteAccountDialog.ts
│       │   │   ├── HomeImports.ts
│       │   │   ├── MFAConfig.ts
│       │   │   ├── NewDocMethods.ts
│       │   │   ├── Notifications.ts
│       │   │   └── ProductUpgrades.ts
│       │   └── widgets/
│       │       └── AssistantPopup.ts
│       ├── common/
│       │   └── version.ts
│       ├── server/
│       │   ├── declarations.d.ts
│       │   ├── lib/
│       │   │   ├── create.ts
│       │   │   ├── globalConfig.ts
│       │   │   └── loginSystems.ts
│       │   ├── prometheus-exporter.ts
│       │   └── server.ts
│       └── tsconfig.json
├── test/
│   ├── assistant/
│   │   ├── data/
│   │   │   └── formula-dataset-index.csv
│   │   └── v1/
│   │       ├── runCompletion.js
│   │       └── runCompletion_impl.ts
│   ├── chai-as-promised.js
│   ├── client/
│   │   ├── clientUtil.js
│   │   ├── components/
│   │   │   ├── Layout.js
│   │   │   ├── WidgetFrame.ts
│   │   │   ├── commands.js
│   │   │   └── sampleLayout.js
│   │   ├── lib/
│   │   │   ├── ACIndex.ts
│   │   │   ├── Delay.js
│   │   │   ├── DocSchemaImport.ts
│   │   │   ├── ImportSourceElement.ts
│   │   │   ├── ObservableMap.js
│   │   │   ├── ObservableSet.js
│   │   │   ├── PluginApi.ts
│   │   │   ├── SafeBrowser.ts
│   │   │   ├── Signal.ts
│   │   │   ├── UrlState.ts
│   │   │   ├── chartUtil.ts
│   │   │   ├── dispose.js
│   │   │   ├── dom.js
│   │   │   ├── domAsync.ts
│   │   │   ├── koArray.js
│   │   │   ├── koArrayWrap.ts
│   │   │   ├── koDom.js
│   │   │   ├── koDomScrolly.js
│   │   │   ├── koForm.js
│   │   │   ├── koUtil.js
│   │   │   ├── localStorageObs.ts
│   │   │   ├── localization.ts
│   │   │   ├── nameUtils.ts
│   │   │   ├── sanitizeUrl.ts
│   │   │   ├── sortUtil.ts
│   │   │   ├── textUtils.ts
│   │   │   ├── timeUtils.ts
│   │   │   └── urlUtils.ts
│   │   ├── models/
│   │   │   ├── ColumnFilter.ts
│   │   │   ├── TreeModel.ts
│   │   │   ├── gristUrlState.ts
│   │   │   ├── modelUtil.js
│   │   │   ├── rowset.js
│   │   │   └── rowuid.js
│   │   ├── shortcuts/
│   │   │   ├── excel.js
│   │   │   └── gsMac.js
│   │   ├── ui/
│   │   │   ├── DocumentSettings.ts
│   │   │   ├── RelativeDatesOptions.ts
│   │   │   └── UserImage.ts
│   │   └── ui2018/
│   │       └── cssVars.ts
│   ├── client-harness/
│   │   └── client.js
│   ├── common/
│   │   ├── ACLPermissions.ts
│   │   ├── AsyncCreate.ts
│   │   ├── BigInt.ts
│   │   ├── BinaryIndexedTree.js
│   │   ├── ChoiceListParser.ts
│   │   ├── CircularArray.js
│   │   ├── ColumnFilterFunc.ts
│   │   ├── DocActions.ts
│   │   ├── DocSchemaImport.ts
│   │   ├── InactivityTimer.ts
│   │   ├── Interval.ts
│   │   ├── KeyedMutex.ts
│   │   ├── MemBuffer.js
│   │   ├── NumberFormat.ts
│   │   ├── NumberParse.ts
│   │   ├── PluginInstance.ts
│   │   ├── RecentItems.js
│   │   ├── RefCountMap.ts
│   │   ├── RelativeDates.ts
│   │   ├── SortFunc.ts
│   │   ├── StringUnion.ts
│   │   ├── TableData.ts
│   │   ├── Telemetry.ts
│   │   ├── ThemePrefs.ts
│   │   ├── ValueFormatter.ts
│   │   ├── ValueGuesser.ts
│   │   ├── airtable/
│   │   │   ├── AirtableAPI.ts
│   │   │   ├── AirtableDataImporter.ts
│   │   │   └── AirtableSchemaImporter.ts
│   │   ├── arraySplice.js
│   │   ├── csvFormat.ts
│   │   ├── getTableTitle.ts
│   │   ├── gristUrls.ts
│   │   ├── gutil.js
│   │   ├── gutil2.ts
│   │   ├── marshal.js
│   │   ├── parseDate.ts
│   │   ├── promises.js
│   │   ├── roles.ts
│   │   ├── serializeTiming.js
│   │   ├── sortTiming.js
│   │   ├── timeFormat.js
│   │   └── tsvFormat.ts
│   ├── declarations.d.ts
│   ├── deployment/
│   │   ├── ActionLog.ts
│   │   ├── ChoiceList.ts
│   │   ├── DuplicateDocument.ts
│   │   ├── Fork.ts
│   │   ├── HomeIntro.ts
│   │   ├── Pages.ts
│   │   ├── README.md
│   │   ├── ReferenceColumns.ts
│   │   ├── ReferenceList.ts
│   │   └── Smoke.ts
│   ├── fixtures/
│   │   ├── docs/
│   │   │   ├── ACL-Test.grist
│   │   │   ├── ActiveDoc-sqlite.grist
│   │   │   ├── AllColumns.grist
│   │   │   ├── ApiDataRecordsTest.grist
│   │   │   ├── AttachmentsJsonMigration.grist
│   │   │   ├── BadRules.grist
│   │   │   ├── BlobMigrationV1.grist
│   │   │   ├── BlobMigrationV16.grist
│   │   │   ├── BlobMigrationV17.grist
│   │   │   ├── BlobMigrationV2.grist
│   │   │   ├── BlobMigrationV3.grist
│   │   │   ├── BlobMigrationV4.grist
│   │   │   ├── BlobMigrationV5.grist
│   │   │   ├── BlobMigrationV6.grist
│   │   │   ├── BlobMigrationV7.grist
│   │   │   ├── BlobMigrationV8.grist
│   │   │   ├── BlobMigrationV9.grist
│   │   │   ├── CCTransactions.grist
│   │   │   ├── CC_Statement.grist
│   │   │   ├── CC_Summaries-v2.grist
│   │   │   ├── CC_Summaries-v6.grist
│   │   │   ├── CC_Summaries.grist
│   │   │   ├── CardView.grist
│   │   │   ├── ChartData.grist
│   │   │   ├── Class Enrollment.grist
│   │   │   ├── Comments_44.grist
│   │   │   ├── CopyOptions.grist
│   │   │   ├── CopyPaste.grist
│   │   │   ├── CopyPaste2.grist
│   │   │   ├── Countries-Print.grist
│   │   │   ├── Covid-19.grist
│   │   │   ├── Currencies.grist
│   │   │   ├── CursorWithRefLists1.grist
│   │   │   ├── CustomWidget.grist
│   │   │   ├── DefaultValuesV5.grist
│   │   │   ├── DefaultValuesV6.grist
│   │   │   ├── DefaultValuesV7.grist
│   │   │   ├── DefaultValuesV8.grist
│   │   │   ├── DefaultValuesV9.grist
│   │   │   ├── DeleteColumnsUndo.grist
│   │   │   ├── DownmigrateTest.grist
│   │   │   ├── DropdownCondition.grist
│   │   │   ├── Excel.grist
│   │   │   ├── ExemptFromFilterBug.grist
│   │   │   ├── Exports.grist
│   │   │   ├── ExternalAttachmentsInvalidStoreId.grist
│   │   │   ├── Favorite_Films.grist
│   │   │   ├── Favorite_Films_Raw.grist
│   │   │   ├── Favorite_Films_With_Linked_Ref.grist
│   │   │   ├── FetchSelectedOptions.grist
│   │   │   ├── FieldSettings.grist
│   │   │   ├── FilmsWithImages.grist
│   │   │   ├── FilterByComplexCellValues.grist
│   │   │   ├── FilterLinkChain.grist
│   │   │   ├── FilterTest.grist
│   │   │   ├── Grist Basics.grist
│   │   │   ├── GristNewUserInfo.grist
│   │   │   ├── Hello.grist
│   │   │   ├── Hooks-v37.grist
│   │   │   ├── ImportReferences.grist
│   │   │   ├── InvalidValues.grist
│   │   │   ├── Investment Research (smaller).grist
│   │   │   ├── Investment Research.grist
│   │   │   ├── Landlord.grist
│   │   │   ├── LastPosition.grist
│   │   │   ├── LinkChain.grist
│   │   │   ├── LongList.grist
│   │   │   ├── ManyRefs.grist
│   │   │   ├── Memos-v34.grist
│   │   │   ├── NumericFormatting.grist
│   │   │   ├── Pages-v19.grist
│   │   │   ├── Pages.grist
│   │   │   ├── PasteParsing.grist
│   │   │   ├── RawSummaryTables.grist
│   │   │   ├── Ref-AC-Test.grist
│   │   │   ├── Ref-List-AC-Test.grist
│   │   │   ├── RemoveTransformColumns.grist
│   │   │   ├── SchoolsSample.grist
│   │   │   ├── SelectByRefList.grist
│   │   │   ├── SelectBySummary.grist
│   │   │   ├── SelectBySummaryRef.grist
│   │   │   ├── SelectionSummary.grist
│   │   │   ├── ShiftSelection.grist
│   │   │   ├── SortDates.grist
│   │   │   ├── SortFilterIconTest.grist
│   │   │   ├── SummarizeByRef.grist
│   │   │   ├── SummaryRulesBug.grist
│   │   │   ├── SummaryTableFormula.grist
│   │   │   ├── TabBar.grist
│   │   │   ├── Teams.grist
│   │   │   ├── TypeConversions.grist
│   │   │   ├── TypeEncoding.grist
│   │   │   ├── Widgets.grist
│   │   │   ├── World-v0.grist
│   │   │   ├── World-v1.grist
│   │   │   ├── World-v10.grist
│   │   │   ├── World-v11.grist
│   │   │   ├── World-v12.grist
│   │   │   ├── World-v13.grist
│   │   │   ├── World-v14.grist
│   │   │   ├── World-v15.grist
│   │   │   ├── World-v18.grist
│   │   │   ├── World-v20.grist
│   │   │   ├── World-v24.grist
│   │   │   ├── World-v25.grist
│   │   │   ├── World-v3.grist
│   │   │   ├── World-v33.grist
│   │   │   ├── World-v39.grist
│   │   │   ├── World-v8.grist
│   │   │   ├── World.grist
│   │   │   ├── WorldSQLDB.grist
│   │   │   ├── WorldUndo.grist
│   │   │   ├── doctour.grist
│   │   │   ├── selectBy.grist
│   │   │   └── video/
│   │   │       ├── ACME Orders.grist
│   │   │       ├── Afterschool Program.grist
│   │   │       ├── Candidates.grist
│   │   │       ├── Employees HomePage.grist
│   │   │       ├── Employees.grist
│   │   │       ├── Leases.grist
│   │   │       └── Lightweight CRM.grist
│   │   ├── export-csv/
│   │   │   ├── CCTransactions-DBA-desc.csv
│   │   │   ├── CCTransactions.csv
│   │   │   ├── choice.csv
│   │   │   ├── date.csv
│   │   │   ├── datetime.csv
│   │   │   ├── field-options.csv
│   │   │   ├── filtered-ref-list.csv
│   │   │   ├── filters-manual.csv
│   │   │   ├── filters-saved.csv
│   │   │   ├── hidden-text.csv
│   │   │   ├── integer.csv
│   │   │   ├── many-rows.csv
│   │   │   ├── numeric.csv
│   │   │   ├── order-color-desc.csv
│   │   │   ├── order-color-manual.csv
│   │   │   ├── order-color-place.csv
│   │   │   ├── order-manual.csv
│   │   │   ├── reference.csv
│   │   │   ├── text.csv
│   │   │   └── toggle.csv
│   │   ├── export-dsv/
│   │   │   ├── CCTransactions.dsv
│   │   │   └── text.dsv
│   │   ├── export-tsv/
│   │   │   ├── CCTransactions.tsv
│   │   │   └── text.tsv
│   │   ├── export-xlsx/
│   │   │   ├── CC_Statement.xlsx
│   │   │   ├── CC_Summaries.xlsx
│   │   │   ├── Currencies.xlsx
│   │   │   ├── Excel.xlsx
│   │   │   ├── Exports.xlsx
│   │   │   └── World-v0.xlsx
│   │   ├── plugins/
│   │   │   ├── .jshintrc
│   │   │   ├── browserInstalledPlugins/
│   │   │   │   └── plugins/
│   │   │   │       ├── browser-GristDocAPI/
│   │   │   │       │   ├── main.js
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── custom-section/
│   │   │   │       │   ├── index-bis.html
│   │   │   │       │   ├── index.html
│   │   │   │       │   ├── main.js
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   ├── test-subscribe-api.html
│   │   │   │       │   └── test-subscribe-api.js
│   │   │   │       └── dummy-importer/
│   │   │   │           ├── index.html
│   │   │   │           ├── main.js
│   │   │   │           ├── manifest.yml
│   │   │   │           ├── node/
│   │   │   │           │   └── main.js
│   │   │   │           ├── sandbox/
│   │   │   │           │   └── main.py
│   │   │   │           └── script.js
│   │   │   ├── builtInPlugins/
│   │   │   │   └── plugins/
│   │   │   │       ├── 2/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── experimental-plugin/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── invalid-contrib-point/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── long-call/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── missing-component/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── missing-safePython/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── safePython-deactivate-fast/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── testing-function-call-plugin/
│   │   │   │       │   ├── backend.js
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── valid-file-parser/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── valid-import-source/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── wrong-json/
│   │   │   │       │   └── manifest.json
│   │   │   │       └── wrong-yaml/
│   │   │   │           └── manifest.yml
│   │   │   └── installedPlugins/
│   │   │       └── plugins/
│   │   │           ├── node-GristDocAPI/
│   │   │           │   ├── TestSubscribe.js
│   │   │           │   ├── main.js
│   │   │           │   └── manifest.yml
│   │   │           ├── node-fail/
│   │   │           │   ├── main.js
│   │   │           │   └── manifest.yml
│   │   │           ├── node-mini-csv/
│   │   │           │   ├── manifest.yml
│   │   │           │   └── nodebox/
│   │   │           │       └── main.js
│   │   │           ├── node-wrong-message/
│   │   │           │   ├── main.js
│   │   │           │   └── manifest.yml
│   │   │           └── valid-import-source/
│   │   │               └── manifest.yml
│   │   ├── projects/
│   │   │   ├── AddNewButton.ts
│   │   │   ├── ApiKey.ts
│   │   │   ├── ColorSelect.ts
│   │   │   ├── ColumnFilterMenu.ts
│   │   │   ├── DocMenu.ts
│   │   │   ├── DocumentSettings.ts
│   │   │   ├── ErrorNotify.ts
│   │   │   ├── Icons.ts
│   │   │   ├── Importer.ts
│   │   │   ├── Mentions.ts
│   │   │   ├── MultiSelector.ts
│   │   │   ├── OnBoardingPopups.ts
│   │   │   ├── PagePanels.ts
│   │   │   ├── PageWidgetPicker.ts
│   │   │   ├── PagesComponent.ts
│   │   │   ├── ParseOptions.ts
│   │   │   ├── ProgressIndicator.ts
│   │   │   ├── Selects.ts
│   │   │   ├── TreeViewComponent.ts
│   │   │   ├── UI2018.ts
│   │   │   ├── UserImage.ts
│   │   │   ├── UserManager.ts
│   │   │   ├── contextMenu.ts
│   │   │   ├── editableLabel.ts
│   │   │   ├── forms.ts
│   │   │   ├── helpers/
│   │   │   │   ├── MockUserAPI.ts
│   │   │   │   ├── Pages.ts
│   │   │   │   ├── ParseOptionsData.ts
│   │   │   │   ├── States.ts
│   │   │   │   ├── gristStyles.ts
│   │   │   │   ├── widgetPicker.ts
│   │   │   │   └── withLocale.ts
│   │   │   ├── modals.ts
│   │   │   ├── mouseDrag.ts
│   │   │   ├── resizeHandle.ts
│   │   │   ├── searchDropdown.ts
│   │   │   ├── sessionObs.ts
│   │   │   ├── simpleList.ts
│   │   │   ├── template.html
│   │   │   ├── tokenfield.ts
│   │   │   ├── tooltips.ts
│   │   │   ├── transitions.ts
│   │   │   ├── webpack-test-server.ts
│   │   │   └── webpack.config.js
│   │   ├── saml/
│   │   │   ├── keycloak.pem
│   │   │   ├── saml-login
│   │   │   ├── saml-logout
│   │   │   ├── saml.crt
│   │   │   └── saml.key
│   │   ├── sites/
│   │   │   ├── config/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── deferred-ready/
│   │   │   │   └── index.html
│   │   │   ├── embed/
│   │   │   │   └── embed.html
│   │   │   ├── fetchSelectedOptions/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── filter/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── hello/
│   │   │   │   └── index.html
│   │   │   ├── paste/
│   │   │   │   └── paste.html
│   │   │   ├── probe/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── readout/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── types/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── types-raw-refs/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── types-rest-api/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   └── zap/
│   │   │       ├── index.html
│   │   │       └── page.js
│   │   └── uploads/
│   │       ├── BooleanData.xlsx
│   │       ├── CCTransactions.csv
│   │       ├── ChartData-Sort_Test.csv
│   │       ├── ChartData.csv
│   │       ├── Cities.csv
│   │       ├── CodeEditor.test.csv
│   │       ├── ColumnFilterData_A.csv
│   │       ├── ColumnFilterData_B.csv
│   │       ├── DateTimeData.xlsx
│   │       ├── EmptyDate.csv
│   │       ├── FileUploadData.csv
│   │       ├── ImportReferences-Tasks.csv
│   │       ├── SchoolData.csv
│   │       ├── StudentData.csv
│   │       ├── UploadedData1.csv
│   │       ├── UploadedData1Extended.csv
│   │       ├── UploadedData2.csv
│   │       ├── UploadedData2Extended.csv
│   │       ├── UploadedData3.csv
│   │       ├── UploadedDataEmpty.csv
│   │       ├── World-v0.xlsx
│   │       ├── World-v1.xlsx
│   │       ├── cities.jgrist
│   │       ├── cities_broken.jgrist
│   │       ├── dirtyNames.json
│   │       ├── empty_data.jgrist
│   │       ├── empty_excel.xlsx
│   │       ├── formatted_numbers.csv
│   │       ├── homicide_rates.xlsx
│   │       ├── htmlfile.html
│   │       ├── mixed_dates.csv
│   │       ├── name_references.csv
│   │       ├── names.json
│   │       ├── simple_array.json
│   │       ├── spotifyGetSeveralAlbums.json
│   │       ├── unicode_headers.csv
│   │       ├── unicode_headers.xlsx
│   │       └── video/
│   │           └── investment-data.xlsx
│   ├── gen-server/
│   │   ├── ApiServer.ts
│   │   ├── ApiServerAccess.ts
│   │   ├── ApiServerBenchmark.ts
│   │   ├── ApiServerBugs.ts
│   │   ├── ApiSession.ts
│   │   ├── AuthCaching.ts
│   │   ├── SqliteSettings.ts
│   │   ├── UpdateChecks.ts
│   │   ├── apiUtils.ts
│   │   ├── lib/
│   │   │   ├── DocApiForwarder.ts
│   │   │   ├── DocPrefs.ts
│   │   │   ├── DocWorkerMap.ts
│   │   │   ├── HealthCheck.ts
│   │   │   ├── HomeDBCaches.ts
│   │   │   ├── HomeDBManager.ts
│   │   │   ├── Housekeeper.ts
│   │   │   ├── emails.ts
│   │   │   ├── everyone.ts
│   │   │   ├── homedb/
│   │   │   │   ├── GroupsManager.ts
│   │   │   │   └── UsersManager.ts
│   │   │   ├── limits.ts
│   │   │   ├── listing.ts
│   │   │   ├── mergedOrgs.ts
│   │   │   ├── prefs.ts
│   │   │   ├── previewer.ts
│   │   │   ├── removedAt.ts
│   │   │   ├── scrubUserFromOrg.ts
│   │   │   ├── suspension.ts
│   │   │   └── urlIds.ts
│   │   ├── migrations.ts
│   │   ├── seed.ts
│   │   └── testUtils.ts
│   ├── init-mocha-webdriver.js
│   ├── nbrowser/
│   │   ├── AccessRules1.ts
│   │   ├── AccessRules2.ts
│   │   ├── AccessRules3.ts
│   │   ├── AccessRules4.ts
│   │   ├── AccessRulesAttrs.ts
│   │   ├── AccessRulesIntro.ts
│   │   ├── AccessRulesSchemaEdit.ts
│   │   ├── AccessibilityModal.ts
│   │   ├── ActionLog.ts
│   │   ├── ActiveUserList.ts
│   │   ├── AdminPanel.ts
│   │   ├── AdminPanelTools.ts
│   │   ├── AirtableImport.ts
│   │   ├── ApiConsole.ts
│   │   ├── AttachedCustomWidget.ts
│   │   ├── AttachmentsLinking.ts
│   │   ├── AttachmentsTransfer.ts
│   │   ├── AttachmentsWidget.ts
│   │   ├── AuthProvider.ts
│   │   ├── AuthProviderGetGrist.ts
│   │   ├── BehavioralPrompts.ts
│   │   ├── Boot.ts
│   │   ├── BundleActions.ts
│   │   ├── CardView.ts
│   │   ├── CellColor.ts
│   │   ├── CellFormat.ts
│   │   ├── ChartView1.ts
│   │   ├── Choice.ts
│   │   ├── ChoiceList.ts
│   │   ├── ClientUnitTests.ntest.js
│   │   ├── CodeEditor.ntest.js
│   │   ├── ColumnFilterMenu.ts
│   │   ├── ColumnFilterMenu2.ts
│   │   ├── ColumnFilterMenu3.ts
│   │   ├── ColumnOps.ntest.js
│   │   ├── ColumnTransform.ts
│   │   ├── Comments.ts
│   │   ├── Comparison.ts
│   │   ├── CopyPaste.ts
│   │   ├── CopyPaste2.ntest.js
│   │   ├── CopyPasteColumnOptions.ts
│   │   ├── CopyPasteFiles.ts
│   │   ├── CopyPasteLinked.ts
│   │   ├── CopyWithHeaders.ts
│   │   ├── CursorSaving.ts
│   │   ├── CustomView.ts
│   │   ├── CustomWidgets.ts
│   │   ├── CustomWidgetsConfig.ts
│   │   ├── DateEditor.ts
│   │   ├── Dates.ntest.js
│   │   ├── DeleteColumnsUndo.ts
│   │   ├── DescriptionColumn.ts
│   │   ├── DescriptionWidget.ts
│   │   ├── DetailView.ntest.js
│   │   ├── DetailView.ts
│   │   ├── DocTour.ts
│   │   ├── DocTutorial.ts
│   │   ├── DocTypeConversion.ts
│   │   ├── DocUsageTracking.ts
│   │   ├── DropdownConditionEditor.ts
│   │   ├── DuplicateDocument.ts
│   │   ├── DuplicatePage.ts
│   │   ├── Export.ntest.js
│   │   ├── ExportSection.ts
│   │   ├── Features.ts
│   │   ├── FieldConfigTab.ntest.js
│   │   ├── FieldEditor.ts
│   │   ├── FieldSettings.ntest.js
│   │   ├── FieldSettings2.ts
│   │   ├── FillLinkedRecords.ntest.js
│   │   ├── FillSelectionDown.ts
│   │   ├── FilterLinkChain.ts
│   │   ├── FilteringBugs.ts
│   │   ├── Fork.ts
│   │   ├── FormView1.ts
│   │   ├── FormView2.ts
│   │   ├── FormsUrlValues.ts
│   │   ├── FormulaAutocomplete.ts
│   │   ├── Formulas.ts
│   │   ├── GridOptions.ntest.js
│   │   ├── GridViewBugs.ts
│   │   ├── GridViewNewColumnMenu.ts
│   │   ├── GridViewNewColumnMenuDateHelpers.ts
│   │   ├── GridViewNewColumnMenuUtils.ts
│   │   ├── HeaderColor.ts
│   │   ├── Health.ntest.js
│   │   ├── HomeIntro.ts
│   │   ├── HomeIntroWithoutPlaygound.ts
│   │   ├── ImportReferences.ts
│   │   ├── Importer.ts
│   │   ├── Importer2.ts
│   │   ├── LanguageSettings.ts
│   │   ├── LazyLoad.ts
│   │   ├── LeftPanel.ts
│   │   ├── LinkingBidirectional.ts
│   │   ├── LinkingErrors.ts
│   │   ├── LinkingSelector.ts
│   │   ├── Localization.ts
│   │   ├── MultiColumn.ts
│   │   ├── NewDocument.ntest.js
│   │   ├── NumericEditor.ts
│   │   ├── OnDemand.ts
│   │   ├── Pages.ts
│   │   ├── Printing.ts
│   │   ├── Properties.ntest.js
│   │   ├── ProposedChangesPage.ts
│   │   ├── RawData.ts
│   │   ├── RecordCards.ts
│   │   ├── RecordLayout.ts
│   │   ├── RefNumericChange.ts
│   │   ├── RefTransforms.ts
│   │   ├── ReferenceColumns.ts
│   │   ├── ReferenceList.ts
│   │   ├── RegionFocusSwitcher.ts
│   │   ├── RemoveTransformColumns.ts
│   │   ├── RightPanel.ts
│   │   ├── RightPanelSelectBy.ts
│   │   ├── RowHeights.ts
│   │   ├── RowMenu.ts
│   │   ├── SavePosition.ntest.js
│   │   ├── Search.ts
│   │   ├── Search2.ts
│   │   ├── Search3.ts
│   │   ├── SearchBar.ntest.ts
│   │   ├── SectionFilter.ts
│   │   ├── SelectBy.ts
│   │   ├── SelectByRefList.ts
│   │   ├── SelectByRightPanel.ts
│   │   ├── SelectBySummary.ts
│   │   ├── SelectBySummaryRef.ts
│   │   ├── SelectionSummary.ts
│   │   ├── ShiftSelection.ts
│   │   ├── Smoke.ts
│   │   ├── SortDates.ntest.js
│   │   ├── SortEditSave.ntest.js
│   │   ├── SortFilterSectionOptions.ts
│   │   ├── SortPositions.ts
│   │   ├── Summaries.ntest.js
│   │   ├── SupportGrist.ts
│   │   ├── TermsOfService.ts
│   │   ├── TextEditor.ntest.js
│   │   ├── Themes.ts
│   │   ├── Timing.ts
│   │   ├── ToggleColumns.ts
│   │   ├── TokenField.ts
│   │   ├── TwoWayReference.ts
│   │   ├── TypeChange.ntest.js
│   │   ├── UndoJumps.ntest.js
│   │   ├── UploadLimits.ts
│   │   ├── UserManager.ts
│   │   ├── UserManager2.ts
│   │   ├── VersionUpdateBanner.ts
│   │   ├── ViewConfigTab.ntest.js
│   │   ├── ViewLayout.ts
│   │   ├── ViewLayoutCollapse.ts
│   │   ├── ViewLayoutUtils.ts
│   │   ├── Views.ntest.js
│   │   ├── VisibleFieldsConfig.ts
│   │   ├── WebhookOverflow.ts
│   │   ├── WebhookPage.ts
│   │   ├── aclTestUtils.ts
│   │   ├── chartViewTestUtils.ts
│   │   ├── customUtil.ts
│   │   ├── disabledAt.ts
│   │   ├── duplicateWidget.ts
│   │   ├── elementUtils.ts
│   │   ├── externalAttachmentsHelpers.ts
│   │   ├── formTools.ts
│   │   ├── gristUtil-nbrowser.js
│   │   ├── gristUtils.ts
│   │   ├── gristWebDriverUtils.ts
│   │   ├── homeUtil.ts
│   │   ├── importerTestUtils.ts
│   │   ├── links.ts
│   │   ├── saveViewSection.ts
│   │   ├── testServer.ts
│   │   ├── testUtils.ts
│   │   ├── webdriverUtils.ts
│   │   ├── webdriverjq-nbrowser.js
│   │   └── webdriverjq.ntest.js
│   ├── nbrowser_with_stubs/
│   │   ├── CreateTeamSite.ts
│   │   └── CustomWidgets.ts
│   ├── projects/
│   │   ├── AccountWidget.ts
│   │   ├── ApiKey.ts
│   │   ├── ColorSelect.ts
│   │   ├── ColumnFilterMenu.ts
│   │   ├── ColumnFilterMenu2.ts
│   │   ├── DateRangeFilter.ts
│   │   ├── DocMenu.ts
│   │   ├── DocumentSettings.ts
│   │   ├── Icons.ts
│   │   ├── Mentions.ts
│   │   ├── MultiSelector.ts
│   │   ├── NotifyBar.ts
│   │   ├── OnBoardingPopups.ts
│   │   ├── PagePanels.ts
│   │   ├── PageWidgetPicker.ts
│   │   ├── PagesComponent.ts
│   │   ├── RangeFilter.ts
│   │   ├── TreeViewComponent.ts
│   │   ├── UI2018.ts
│   │   ├── UserManager.ts
│   │   ├── contextMenu.ts
│   │   ├── editableLabel.ts
│   │   ├── errorPages.ts
│   │   ├── filterUtils.ts
│   │   ├── modals.ts
│   │   ├── mouseDrag.ts
│   │   ├── resizeHandle.ts
│   │   ├── searchDropdown.ts
│   │   ├── sessionObs.ts
│   │   ├── simpleList.ts
│   │   ├── testUtils.ts
│   │   ├── tokenfield.ts
│   │   ├── tooltips.ts
│   │   └── transitions.ts
│   ├── report-why-tests-hang.js
│   ├── server/
│   │   ├── Comm.ts
│   │   ├── PyMomentTest.ts
│   │   ├── Sandbox.ts
│   │   ├── customUtil.ts
│   │   ├── docTools.ts
│   │   ├── generateInitialDocSql.ts
│   │   ├── gristClient.ts
│   │   ├── lib/
│   │   │   ├── ACLFormula.ts
│   │   │   ├── ACLRulesReader.ts
│   │   │   ├── AccessTokens.ts
│   │   │   ├── ActionHistory.ts
│   │   │   ├── ActionHistoryMemory.ts
│   │   │   ├── ActionSummary.ts
│   │   │   ├── ActiveDoc.ts
│   │   │   ├── ActiveDocImport.js
│   │   │   ├── ActiveDocShutdown.ts
│   │   │   ├── AppSettings.ts
│   │   │   ├── Archive.ts
│   │   │   ├── AttachmentFileManager.ts
│   │   │   ├── AttachmentStoreProvider.ts
│   │   │   ├── Authorizer.ts
│   │   │   ├── BundleActions.ts
│   │   │   ├── CommentAccess.ts
│   │   │   ├── CommentAccess2.ts
│   │   │   ├── ConfigBackendAPI.ts
│   │   │   ├── DocSnapshots.ts
│   │   │   ├── DocStorage.js
│   │   │   ├── DocStorageManager.ts
│   │   │   ├── DocStorageMigrations.ts
│   │   │   ├── DocStorageQuery.ts
│   │   │   ├── DocWorkerLoadTracker.ts
│   │   │   ├── ExportsAccessRules.ts
│   │   │   ├── ExternalStorageAttachmentStore.ts
│   │   │   ├── FilesystemAttachmentStore.ts
│   │   │   ├── GranularAccess.ts
│   │   │   ├── GristJobs.ts
│   │   │   ├── GristSockets.ts
│   │   │   ├── HashUtil.ts
│   │   │   ├── HostedMetadataManager.ts
│   │   │   ├── HostedStorageManager.ts
│   │   │   ├── ManyFetches.ts
│   │   │   ├── MemoryPool.ts
│   │   │   ├── MinIOExternalStorage.ts
│   │   │   ├── OIDCConfig.ts
│   │   │   ├── OnDemandActions.ts
│   │   │   ├── OpenAIAssistantV1.ts
│   │   │   ├── Proposals.ts
│   │   │   ├── ProxyAgent.ts
│   │   │   ├── PubSubCache.ts
│   │   │   ├── PubSubManager.ts
│   │   │   ├── RowAccess.ts
│   │   │   ├── SQLiteDB.ts
│   │   │   ├── SamlConfig.ts
│   │   │   ├── Scim.ts
│   │   │   ├── TableMetadataLoader.ts
│   │   │   ├── Telemetry.ts
│   │   │   ├── TestingHooks.ts
│   │   │   ├── Throttle.ts
│   │   │   ├── TimeQuery.ts
│   │   │   ├── Triggers.ts
│   │   │   ├── UnhandledErrors.ts
│   │   │   ├── UserAttributes.ts
│   │   │   ├── UserPresence.ts
│   │   │   ├── Webhooks-Proxy.ts
│   │   │   ├── checksumFile.ts
│   │   │   ├── config.ts
│   │   │   ├── configCore.ts
│   │   │   ├── configCoreFileFormats.ts
│   │   │   ├── docapi/
│   │   │   │   ├── DocApiAnonPlayground.ts
│   │   │   │   ├── DocApiAttachments.ts
│   │   │   │   ├── DocApiBugsAndFixes.ts
│   │   │   │   ├── DocApiColumns.ts
│   │   │   │   ├── DocApiCreation.ts
│   │   │   │   ├── DocApiDocuments.ts
│   │   │   │   ├── DocApiDownloads.ts
│   │   │   │   ├── DocApiMisc.ts
│   │   │   │   ├── DocApiOrgLimitFlags.ts
│   │   │   │   ├── DocApiPermissions.ts
│   │   │   │   ├── DocApiQueryParameters.ts
│   │   │   │   ├── DocApiRecords.ts
│   │   │   │   ├── DocApiReverseProxy.ts
│   │   │   │   ├── DocApiSql.ts
│   │   │   │   ├── DocApiTables.ts
│   │   │   │   ├── DocApiWebhooks.ts
│   │   │   │   └── helpers.ts
│   │   │   ├── extractOrg.ts
│   │   │   ├── helpers/
│   │   │   │   ├── PrepareDatabase.ts
│   │   │   │   ├── PrepareFilesystemDirectoryForTests.ts
│   │   │   │   ├── Signal.ts
│   │   │   │   ├── TestProxyServer.ts
│   │   │   │   └── TestServer.ts
│   │   │   ├── idUtils.ts
│   │   │   ├── requestUtils.ts
│   │   │   ├── sandboxUtil.ts
│   │   │   ├── serverUtils.js
│   │   │   ├── serverUtils2.ts
│   │   │   ├── shortDesc.js
│   │   │   ├── updateChecker.ts
│   │   │   └── uploads.ts
│   │   ├── tcpForwarder.ts
│   │   ├── testCleanup.ts
│   │   ├── testUtils.ts
│   │   ├── utils/
│   │   │   ├── CachedFetcher.ts
│   │   │   ├── LogSanitizer.ts
│   │   │   └── streams.ts
│   │   └── wait.ts
│   ├── setupPaths.js
│   ├── split-tests.js
│   ├── testUtils.ts
│   ├── test_env.sh
│   ├── test_under_docker.sh
│   ├── timings/
│   │   ├── nbrowser.txt
│   │   └── server.txt
│   ├── tsconfig.json
│   ├── upgradeDocument
│   ├── upgradeDocumentImpl.ts
│   ├── utils.js
│   └── xunit-file.js
├── tsconfig-ext.json
├── tsconfig-prod.json
├── tsconfig.eslint.json
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
# explicitly list the files needed by docker.
*
!package.json
!yarn.lock
!tsconfig-ext.json
!tsconfig-prod.json
!tsconfig.json
!stubs
!app
!buildtools
!static
!bower_components
!sandbox
!plugins
!test
!ext
**/_build


================================================
FILE: .editorconfig
================================================
# EditorConfig is awesome: https://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*.{ts,js,py}]
end_of_line = lf
insert_final_newline = true
charset = utf-8
# indent with 2 spaces
indent_style = space
indent_size = 2
trim_trailing_whitespace = true


================================================
FILE: .git-blame-ignore-revs
================================================
a7eb4d6e60c50375a835a5300b8e6beebcfb8422
3a859ee5454d9c2b9600c9d9a3bd859d65d496bf
9c9c45a6894e80748f0e337d4f164d40b503cae5
a3082e1f8b59b36154e721eb3d17c6504cf63bb8
29541707086d4de9fbe8773f42e0f5f0f845ea54
4faa64a1c0c194b32a804b2e562361ba197d8699
e44e6217bd223873aef2f1aa2df32dc9c17a3abf
e75c8ce28d9470ba5e92ee25aec65869a7051668
b7e9ebed9dddd931a9bf33ae57dccb6423a7b6ae
a893706c5b9659157878318513f6d6cbe025e51d
2f2cd7d60144486e67fc2fe0eaf20fcafcd50f21


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: gristlabs


================================================
FILE: .github/ISSUE_TEMPLATE/00-bug-issue.yml
================================================
# Inspired by PeerTube templates:
# https://github.com/Chocobozzz/PeerTube/blob/3d4d49a23eae71f3ce62cbbd7d93f07336a106b7/.github/ISSUE_TEMPLATE/00-bug-issue.yml
name: 🐛 Bug Report
description: Use this template for reporting a bug
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking time to fill out this bug report!
        Please search among past open/closed issues for a similar one beforehand:
          - https://github.com/gristlabs/grist-core/issues?q=
          - https://community.getgrist.com/

  - type: textarea
    attributes:
      label: Describe the current behavior

  - type: textarea
    attributes:
      label: Steps to reproduce
      value: |
          1.
          2.
          3.

  - type: textarea
    attributes:
      label: Describe the expected behavior

  - type: checkboxes
    attributes:
      label: Where have you encountered this bug?
      options:
        - label: On [docs.getgrist.com](https://docs.getgrist.com)
        - label: On a self-hosted instance 
    validations:
      required: true

  - type: textarea
    attributes:
      label: Instance information (when self-hosting only)
      description: In case you self-host, please share information above. You can discard any question you don't know the answer.
      value: |
          * Grist instance:
            * Version:
            * URL (if it's OK for you to share it):
            * Installation mode: docker/kubernetes/...
            * Architecture: single-worker/multi-workers

          * Browser name, version and platforms on which you could reproduce the bug:
          * Link to browser console log if relevant:
          * Link to server log if relevant:


================================================
FILE: .github/ISSUE_TEMPLATE/10-installation-issue.yml
================================================
# Inspired by PeerTube templates:
# https://github.com/Chocobozzz/PeerTube/blob/master/.github/ISSUE_TEMPLATE/10-installation-issue.yml
name: 🛠️ Installation/Upgrade Issue
description: Use this template for installation/upgrade issues
body:
  - type: markdown
    attributes:
      value: |
        Please check first the official documentation for self-hosting: https://support.getgrist.com/self-managed/

  - type: markdown
    attributes:
      value: |
        Please search among past open/closed issues for a similar one beforehand:
          - https://github.com/gristlabs/grist-core/issues?q=
          - https://community.getgrist.com/

  - type: textarea
    attributes:
      label: Describe the problem

  - type: textarea
    attributes:
      label: Additional information
      value: |
          * Grist version:
          * Grist instance URL:
          * SSO solution used and its version (if relevant):
          * S3 storage solution and its version (if relevant):
          * Docker version (if relevant):
          * NodeJS version (if relevant):
          * Redis version (if relevant):
          * PostgreSQL version (if relevant):


================================================
FILE: .github/ISSUE_TEMPLATE/20-feature-request.yml
================================================
# Inspired by PeerTube templates:
# https://github.com/Chocobozzz/PeerTube/blob/master/.github/ISSUE_TEMPLATE/30-feature-request.yml
---
name: ✨ Feature Request
description: Use this template to ask for new features and suggest new ideas 💡
body:
  - type: markdown
    attributes:
      value: |
        Thanks for taking time to share your ideas!
        Please search among past open/closed issues for a similar one beforehand:
          - https://github.com/gristlabs/grist-core/issues?q=
          - https://community.getgrist.com/

  - type: textarea
    attributes:
      label: Describe the problem to be solved
      description: Provide a clear and concise description of what the problem is

  - type: textarea
    attributes:
      label: Describe the solution you would like
      description: Provide a clear and concise description of what you want to happen


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: 🤷💻🤦 Question/Forum
    url: https://community.getgrist.com/
    about: You can ask and answer other questions here
  - name: 💬 Discord
    url: https://discord.com/invite/MYKpYQ3fbP
    about: Chat with us via Discord for quick Q/A here and sharing tips


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Context

<!-- Please include a summary of the change, with motivation and context -->
<!-- Bonus: if you are comfortable writing one, please insert a user-story https://en.wikipedia.org/wiki/User_story#Common_templates -->

## Proposed solution

<!-- Describe here how you address the issue -->

## Related issues

<!-- If suggesting a new feature or change, please discuss it in an issue first -->
<!-- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!-- If this does not solve entirely the issue, make also a checklist of what is done or not: -->

## Has this been tested?

<!-- Put an `x` in the box that applies: -->

- [ ] 👍 yes, I added tests to the test suite
- [ ] 💭 no, because this PR is a draft and still needs work
- [ ] 🙅 no, because this is not relevant here
- [ ] 🙋 no, because I need help <!-- Detail how we can help you -->

## Screenshots / Screencasts

<!-- delete if not relevant -->


================================================
FILE: .github/cla/individual-cla.md
================================================
# Individual Contributor License Agreement ("Agreement"), v1.0

(Based on https://www.apache.org/licenses/icla.pdf by the Apache
Foundation. Contact support@getgrist.com if you wish to execute a
Corporate CLA.)

Thank you for your interest in contributing to software projects made
available by Grist Labs Inc ("Grist"). To clarify the intellectual
property license granted with Contributions from any person or entity,
Grist must have on file a signed Contributor License Agreement ("CLA")
from each Contributor, indicating agreement with the license terms
below. This agreement is for your protection as a Contributor as well
as the protection of Grist and its users. It does not change your
rights to use your own Contributions for any other purpose.

Contributions entirely composed of commits with authorship at
`*.gouv.fr` domains fall outside the scope of this agreement.

You accept and agree to the following terms and conditions for Your
Contributions (present and future) that you submit to Grist. Except
for the license granted herein to Grist and recipients of software
distributed by Grist, You reserve all right, title, and interest in
and to Your Contributions.

1. Definitions.

   "You" (or "Your") shall mean the copyright owner or legal entity
   authorized by the copyright owner that is making this Agreement
   with Grist. For legal entities, the entity making a Contribution
   and all other entities that control, are controlled by, or are
   under common control with that entity are considered to be a single
   Contributor. For the purposes of this definition, "control" means
   (i) the power, direct or indirect, to cause the direction or
   management of such entity, whether by contract or otherwise, or
   (ii) ownership of fifty percent (50%) or more of the outstanding
   shares, or (iii) beneficial ownership of such entity.

   "Contribution" shall mean any original work of authorship,
   including any modifications or additions to an existing work, that
   is intentionally submitted by You to Grist for inclusion in, or
   documentation of, any of the products owned or managed by Grist
   (the "Work"). For the purposes of this definition, "submitted"
   means any form of electronic, verbal, or written communication sent
   to Grist or its representatives, including but not limited to
   communication on electronic mailing lists, source code control
   systems, and issue tracking systems that are managed by, or on
   behalf of, Grist for the purpose of discussing and improving the
   Work, but excluding communication that is conspicuously marked or
   otherwise designated in writing by You as "Not a Contribution."

2. Grant of Copyright License.

   Subject to the terms and conditions of this Agreement, You hereby
   grant to Grist and to recipients of software distributed by Grist a
   perpetual, worldwide, non-exclusive, no-charge, royalty-free,
   irrevocable copyright license to reproduce, prepare derivative
   works of, publicly display, publicly perform, sublicense, and
   distribute Your Contributions and such derivative works.

3. Grant of Patent License.

   Subject to the terms and conditions of this Agreement, You hereby
   grant to Grist and to recipients of software distributed by Grist a
   perpetual, worldwide, non-exclusive, no-charge, royalty-free,
   irrevocable (except as stated in this section) patent license to
   make, have made, use, offer to sell, sell, import, and otherwise
   transfer the Work, where such license applies only to those patent
   claims licensable by You that are necessarily infringed by Your
   Contribution(s) alone or by combination of Your Contribution(s)
   with the Work to which such Contribution(s) was submitted. If any
   entity institutes patent litigation against You or any other entity
   (including a cross-claim or counterclaim in a lawsuit) alleging
   that your Contribution, or the Work to which you have contributed,
   constitutes direct or contributory patent infringement, then any
   patent licenses granted to that entity under this Agreement for
   that Contribution or Work shall terminate as of the date such
   litigation is filed.

4. You represent that you are legally entitled to grant the above
   license. If your employer(s) has rights to intellectual property
   that you create that includes your Contributions, you represent
   that you have received permission to make Contributions on behalf
   of that employer, that your employer has waived such rights for
   your Contributions to Grist, or that your employer has executed a
   separate Corporate CLA with Grist.

5. You represent that each of Your Contributions is Your original
   creation (see section 7 for submissions on behalf of others). You
   represent that Your Contribution submissions include complete
   details of any third-party license or other restriction (including,
   but not limited to, related patents and trademarks) of which you
   are personally aware and which are associated with any part of Your
   Contributions.

6. You are not expected to provide support for Your Contributions,
   except to the extent You desire to provide support. You may provide
   support for free, for a fee, or not at all. Unless required by
   applicable law or agreed to in writing, You provide Your
   Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
   OF ANY KIND, either express or implied, including, without
   limitation, any warranties or conditions of TITLE,
   NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR
   PURPOSE.

7. Should You wish to submit work that is not Your original creation,
   You may submit it to Grist separately from any Contribution,
   identifying the complete details of its source and of any license
   or other restriction (including, but not limited to, related
   patents, trademarks, and license agreements) of which you are
   personally aware, and conspicuously marking the work as "Submitted
   on behalf of a third-party: [named here]".

8. You agree to notify Grist of any facts or circumstances of which
   you become aware that would make these representations inaccurate
   in any respect.


================================================
FILE: .github/cla/signatures.json
================================================
{
  "signedContributors": [
    {
      "name": "jordigh",
      "id": 260143,
      "comment_id": 3053400623,
      "created_at": "2025-07-09T17:12:17Z",
      "repoId": 266033395,
      "pullRequestNo": 1682
    },
    {
      "name": "georgegevoian",
      "id": 85144792,
      "comment_id": 3070684406,
      "created_at": "2025-07-14T19:16:24Z",
      "repoId": 266033395,
      "pullRequestNo": 1705
    },
    {
      "name": "scytacki",
      "id": 86016,
      "comment_id": 3071744517,
      "created_at": "2025-07-15T03:10:33Z",
      "repoId": 266033395,
      "pullRequestNo": 1661
    },
    {
      "name": "fflorent",
      "id": 371705,
      "comment_id": 3079056343,
      "created_at": "2025-07-16T15:06:22Z",
      "repoId": 266033395,
      "pullRequestNo": 1683
    },
    {
      "name": "manuhabitela",
      "id": 221253,
      "comment_id": 3083417362,
      "created_at": "2025-07-17T09:55:14Z",
      "repoId": 266033395,
      "pullRequestNo": 1431
    },
    {
      "name": "hexaltation",
      "id": 31125573,
      "comment_id": 3083469227,
      "created_at": "2025-07-17T10:11:19Z",
      "repoId": 266033395,
      "pullRequestNo": 1699
    },
    {
      "name": "paulfitz",
      "id": 118367,
      "comment_id": 3084758085,
      "created_at": "2025-07-17T16:53:25Z",
      "repoId": 266033395,
      "pullRequestNo": 1711
    },
    {
      "name": "Spoffy",
      "id": 4805393,
      "comment_id": 3090253280,
      "created_at": "2025-07-18T18:01:45Z",
      "repoId": 266033395,
      "pullRequestNo": 1670
    },
    {
      "name": "Anany-k",
      "id": 13112955,
      "comment_id": 3095980233,
      "created_at": "2025-07-21T09:55:52Z",
      "repoId": 266033395,
      "pullRequestNo": 1716
    },
    {
      "name": "vviers",
      "id": 30295971,
      "comment_id": 3097269575,
      "created_at": "2025-07-21T15:34:37Z",
      "repoId": 266033395,
      "pullRequestNo": 1719
    },
    {
      "name": "guillett",
      "id": 1410356,
      "comment_id": 3113331623,
      "created_at": "2025-07-24T12:43:18Z",
      "repoId": 266033395,
      "pullRequestNo": 1691
    },
    {
      "name": "ogui11aume",
      "id": 31072389,
      "comment_id": 3118009823,
      "created_at": "2025-07-25T14:21:07Z",
      "repoId": 266033395,
      "pullRequestNo": 1653
    },
    {
      "name": "mrdev023",
      "id": 11292703,
      "comment_id": 3155373604,
      "created_at": "2025-08-05T14:02:58Z",
      "repoId": 266033395,
      "pullRequestNo": 1750
    },
    {
      "name": "jonathanperret",
      "id": 300823,
      "comment_id": 3233984142,
      "created_at": "2025-08-28T15:30:02Z",
      "repoId": 266033395,
      "pullRequestNo": 1778
    },
    {
      "name": "berhalak",
      "id": 11277225,
      "comment_id": 3249600375,
      "created_at": "2025-09-03T14:53:49Z",
      "repoId": 266033395,
      "pullRequestNo": 1807
    },
    {
      "name": "Ajay-Satish-01",
      "id": 71289526,
      "comment_id": 3260958924,
      "created_at": "2025-09-06T05:28:40Z",
      "repoId": 266033395,
      "pullRequestNo": 1818
    },
    {
      "name": "dsagal",
      "id": 1091143,
      "comment_id": 3293302321,
      "created_at": "2025-09-15T17:52:16Z",
      "repoId": 266033395,
      "pullRequestNo": 1841
    },
    {
      "name": "tristanrobert",
      "id": 19711088,
      "comment_id": 3297472676,
      "created_at": "2025-09-16T10:20:26Z",
      "repoId": 266033395,
      "pullRequestNo": 1840
    },
    {
      "name": "ohemelaar",
      "id": 19656762,
      "comment_id": 3326880813,
      "created_at": "2025-09-24T07:05:33Z",
      "repoId": 266033395,
      "pullRequestNo": 1830
    },
    {
      "name": "nbush",
      "id": 3422005,
      "comment_id": 3467956044,
      "created_at": "2025-10-30T13:18:24Z",
      "repoId": 266033395,
      "pullRequestNo": 1906
    },
    {
      "name": "SimLV",
      "id": 68837817,
      "comment_id": 3485625062,
      "created_at": "2025-11-04T11:57:57Z",
      "repoId": 266033395,
      "pullRequestNo": 1921
    },
    {
      "name": "SleepyLeslie",
      "id": 142967379,
      "comment_id": 3487547371,
      "created_at": "2025-11-04T18:46:35Z",
      "repoId": 266033395,
      "pullRequestNo": 1922
    },
    {
      "name": "samchencode",
      "id": 62081196,
      "comment_id": 3522061093,
      "created_at": "2025-11-12T13:53:43Z",
      "repoId": 266033395,
      "pullRequestNo": 1935
    },
    {
      "name": "RapidShade",
      "id": 20725534,
      "comment_id": 3533412593,
      "created_at": "2025-11-14T15:54:39Z",
      "repoId": 266033395,
      "pullRequestNo": 1945
    },
    {
      "name": "cfpwastaken",
      "id": 44261356,
      "comment_id": 3694780658,
      "created_at": "2025-12-28T14:18:03Z",
      "repoId": 266033395,
      "pullRequestNo": 2023
    },
    {
      "name": "unknownconstant",
      "id": 14999931,
      "comment_id": 3791801541,
      "created_at": "2026-01-23T18:54:49Z",
      "repoId": 266033395,
      "pullRequestNo": 2067
    },
    {
      "name": "mikhailbogdan-droid",
      "id": 238033187,
      "comment_id": 3823827850,
      "created_at": "2026-01-30T13:42:28Z",
      "repoId": 266033395,
      "pullRequestNo": 2085
    },
    {
      "name": "Vortezz",
      "id": 61989315,
      "comment_id": 3861607282,
      "created_at": "2026-02-06T17:08:18Z",
      "repoId": 266033395,
      "pullRequestNo": 2096
    },
    {
      "name": "kosssi",
      "id": 1135513,
      "comment_id": 4004185474,
      "created_at": "2026-03-05T10:52:21Z",
      "repoId": 266033395,
      "pullRequestNo": 2151
    },
    {
      "name": "webash",
      "id": 6205899,
      "comment_id": 4026041376,
      "created_at": "2026-03-09T18:57:39Z",
      "repoId": 266033395,
      "pullRequestNo": 2161
    },
    {
      "name": "Gwabix",
      "id": 50367170,
      "comment_id": 4060017206,
      "created_at": "2026-03-14T08:23:49Z",
      "repoId": 266033395,
      "pullRequestNo": 2178
    }
  ]
}

================================================
FILE: .github/workflows/cla.yml
================================================
# Workflow body from https://github.com/contributor-assistant/github-action

name: "CLA Assistant"
on:
  issue_comment:
    types: [created]
  pull_request_target:
    types: [opened,closed,synchronize]

permissions:
  actions: write
  contents: write
  pull-requests: write
  statuses: write

jobs:
  CLAAssistant:
    runs-on: ubuntu-latest
    steps:
      - name: "CLA Assistant"
        if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
        uses: contributor-assistant/github-action@v2.6.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          path-to-signatures: '.github/cla/signatures.json'
          path-to-document: 'https://github.com/gristlabs/grist-core/blob/main/.github/cla/individual-cla.md'
          branch: 'main'
          allowlist: github-actions[bot],dependabot[bot]
          lock-pullrequest-aftermerge: false


================================================
FILE: .github/workflows/docker.yml
================================================
name: Push Docker image

on:
  release:
    types: [published]
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:
    inputs:
      tag:
        description: "Tag for the resulting images"
        type: string
        required: True
        default: 'stable'

env:
  TAG: ${{ inputs.tag || 'stable' }}
  DOCKER_HUB_OWNER: ${{ vars.DOCKER_HUB_OWNER || github.repository_owner }}
  PLATFORMS: ${{ vars.PLATFORMS || 'linux/amd64,linux/arm64/v8' }}

jobs:
  push_to_registry:
    name: Push Docker images to Docker Hub
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        image:
          # We build two images, `grist-oss` and `grist`.
          # See https://github.com/gristlabs/grist-core?tab=readme-ov-file#available-docker-images
          - name: "grist-oss"
            repo: "grist-core"
          - name: "grist"
            repo: "grist-ee"
    steps:
      - name: Free some space
        run: |
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/local/lib/android
          sudo rm -rf /usr/local/.ghcup
          sudo rm -rf /usr/share/swift

      - name: Check out the repo
        uses: actions/checkout@v3

      - name: Add a dummy ext/ directory
        run:
          mkdir ext && touch ext/dummy

      - name: Check out the ext/ directory
        if: matrix.image.name != 'grist-oss'
        run: buildtools/checkout-ext-directory.sh ${{ matrix.image.repo }}

      - name: Generate metadata tag input
        id: meta_input
        run: |
          {
            echo "tags<<EOF"
            echo "type=ref,event=branch"
            echo "type=ref,event=pr"
            echo "type=semver,pattern={{version}}"
            echo "type=semver,pattern={{major}}.{{minor}}"
            echo "type=semver,pattern={{major}}"
            echo "${{ env.TAG }}"
            echo "EOF"
          } >> $GITHUB_OUTPUT

      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: |
            ${{ env.DOCKER_HUB_OWNER }}/${{ matrix.image.name }}
          tags: |
            ${{ steps.meta_input.outputs.tags }}

      - name: Docker meta (EE)
        if: ${{ matrix.image.name == 'grist' }}
        id: meta_ee
        uses: docker/metadata-action@v4
        with:
          images: |
            ${{ env.DOCKER_HUB_OWNER }}/grist-ee
          tags: |
            ${{ steps.meta_input.outputs.tags }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Push to Docker Hub
        uses: docker/build-push-action@v2
        with:
          context: .
          build-args: GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING=${{ matrix.image.name == 'grist-oss' && 'false' || 'true' }}
          push: true
          platforms: ${{ env.PLATFORMS }}
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-contexts: ext=ext

      - name: Push Enterprise to Docker Hub
        if: ${{ matrix.image.name == 'grist' }}
        uses: docker/build-push-action@v2
        with:
          context: .
          build-args: |
            BASE_IMAGE=${{ env.DOCKER_HUB_OWNER }}/${{ matrix.image.name}}
            BASE_VERSION=${{ env.TAG }}
            GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING=true
          file: ext/Dockerfile
          platforms: ${{ env.PLATFORMS }}
          push: true
          tags: ${{ steps.meta_ee.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max


================================================
FILE: .github/workflows/docker_latest.yml
================================================
name: Push latest Docker image

on:
  push:
    # Trigger if latest_candidate updates. This is automatically done by another
    # workflow whenever tests pass on main - but events don't chain without using
    # personal access tokens so we just use a cron job.
    branches: [ latest_candidate ]
  schedule:
    # Run at 5:41 UTC daily
    - cron:  '41 5 * * *'
  workflow_dispatch:
    inputs:
      branch:
        description: "Branch from which to create the latest Docker image (default: latest_candidate)"
        type: string
        required: true
        default: latest_candidate
      disable_tests:
        description: "Should the tests be skipped?"
        type: boolean
        required: True
        default: False
      platforms:
        description: "Platforms to build"
        type: choice
        required: True
        options:
          - linux/amd64
          - linux/arm64/v8
          - linux/amd64,linux/arm64/v8
        default: linux/amd64,linux/arm64/v8
      tag:
        description: "Tag for the resulting images"
        type: string
        required: True
        default: 'latest'

env:
  BRANCH: ${{ inputs.branch || 'latest_candidate' }}
  PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64/v8' }}
  TAG: ${{ inputs.tag || 'latest' }}
  DOCKER_HUB_OWNER: ${{ vars.DOCKER_HUB_OWNER || github.repository_owner }}

jobs:
  push_to_registry:
    name: Push latest Docker image to Docker Hub
    runs-on: ubuntu-22.04
    if: ${{ vars.RUN_DAILY_BUILD }}
    strategy:
      matrix:
        python-version: [3.11]
        node-version: [22.x]
        image:
          # We build two images, `grist-oss` and `grist`.
          # See https://github.com/gristlabs/grist-core?tab=readme-ov-file#available-docker-images
          - name: "grist-oss"
            repo: "grist-core"
          - name: "grist"
            repo: "grist-ee"
    steps:
      - name: Build settings
        run: |
          echo "Branch: $BRANCH"
          echo "Platforms: $PLATFORMS"
          echo "Docker Hub Owner: $DOCKER_HUB_OWNER"
          echo "Tag: $TAG"

      - name: Free disk space
        run: |
          echo "Disk space before cleanup:"
          df -h /
          echo "Removing Android SDK..."
          sudo rm -rf /usr/local/lib/android
          df -h /
          echo "Removing .NET..."
          sudo rm -rf /usr/share/dotnet
          df -h /
          echo "Removing Haskell..."
          sudo rm -rf /usr/local/.ghcup
          df -h /
          echo "Removing Swift..."
          sudo rm -rf /usr/share/swift
          df -h /
          echo "Final disk space:"
          df -h /

      - name: Check out the repo
        uses: actions/checkout@v4
        with:
          ref: ${{ env.BRANCH }}

      - name: Add a dummy ext/ directory
        run:
          mkdir ext && touch ext/dummy

      - name: Check out the ext/ directory
        if: matrix.image.name != 'grist-oss'
        run: buildtools/checkout-ext-directory.sh ${{ matrix.image.repo }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Prepare image but do not push it yet
        uses: docker/build-push-action@v2
        with:
          context: .
          load: true
          tags: ${{ env.DOCKER_HUB_OWNER }}/${{ matrix.image.name }}:${{ env.TAG }}
          cache-from: type=gha
          build-contexts: ext=ext

      - name: Use Node.js ${{ matrix.node-version }} for testing
        if: ${{ !inputs.disable_tests }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Set up Python ${{ matrix.python-version }} for testing - maybe not needed
        if: ${{ !inputs.disable_tests }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install Python packages
        if: ${{ !inputs.disable_tests }}
        run: |
          pip install virtualenv
          yarn run install:python

      - name: Install Node.js packages
        if: ${{ !inputs.disable_tests }}
        run: yarn install

      - name: Disable the ext/ directory
        if: ${{ !inputs.disable_tests }}
        run: mv ext/ ext-disabled/

      - name: Build Node.js code
        if: ${{ !inputs.disable_tests }}
        run: yarn run build

      - name: Install Google Chrome and chromedriver
        run: buildtools/install_chrome_for_tests.sh -y

      - name: Run tests with default settings
        if: ${{ !inputs.disable_tests }}
        run: |
          export TEST_IMAGE=${{ env.DOCKER_HUB_OWNER }}/${{ matrix.image.name }}:${{ env.TAG }}
          export VERBOSE=1
          export DEBUG=1
          export MOCHA_WEBDRIVER_HEADLESS=1
          yarn run test:docker

      - name: Run some tests with gvisor and python
        if: ${{ !inputs.disable_tests }}
        run: |
          export TEST_IMAGE=${{ env.DOCKER_HUB_OWNER }}/${{ matrix.image.name }}:${{ env.TAG }}
          export VERBOSE=1
          export DEBUG=1
          export MOCHA_WEBDRIVER_HEADLESS=1
          export GREP_TESTS='should support basic editing'
          export TEST_DOCKER_OPTIONS='-e GRIST_SANDBOX_FLAVOR=gvisor -e PYTHON_VERSION_ON_CREATION=3'
          yarn run test:docker

      - name: Re-enable the ext/ directory
        if: ${{ !inputs.disable_tests }}
        run: mv ext-disabled/ ext/

      - name: Log in to Docker Hub
        uses: docker/login-action@v1 
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Push to Docker Hub
        uses: docker/build-push-action@v2
        with:
          context: .
          build-args: GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING=${{ matrix.image.name == 'grist-oss' && 'false' || 'true' }}
          platforms: ${{ env.PLATFORMS }}
          push: true
          tags: ${{ env.DOCKER_HUB_OWNER }}/${{ matrix.image.name }}:${{ env.TAG }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-contexts: ext=ext

      - name: Push Enterprise to Docker Hub
        if: ${{ matrix.image.name == 'grist' }}
        uses: docker/build-push-action@v2
        with:
          context: .
          build-args: |
            BASE_IMAGE=${{ env.DOCKER_HUB_OWNER }}/${{ matrix.image.name}}
            BASE_VERSION=${{ env.TAG }}
            GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING=true
          file: ext/Dockerfile
          platforms: ${{ env.PLATFORMS }}
          push: true
          tags: ${{ env.DOCKER_HUB_OWNER }}/grist-ee:${{ env.TAG }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  update_latest_branch:
    name: Update latest branch
    runs-on: ubuntu-22.04
    needs: push_to_registry
    steps:
      - name: Check out the repo
        uses: actions/checkout@v2
        with:
          ref: ${{ inputs.latest_branch }}

      - name: Update latest branch
        uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: latest
          force: true


================================================
FILE: .github/workflows/fly-build.yml
================================================
# fly-deploy will be triggered on completion of this workflow to actually deploy the code to fly.io.

name: fly.io Build
on:
  pull_request:
    branches: [ main ]
    types: [labeled, opened, synchronize, reopened]

  # Allows running this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  build:
    name: Build Docker image
    runs-on: ubuntu-22.04
    # Build when the 'preview' label is added, or when PR is updated with this label present.
    if: >
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'pull_request' &&
      contains(github.event.pull_request.labels.*.name, 'preview'))
    steps:
      - uses: actions/checkout@v4
      - name: Build and export Docker image
        id: docker-build
        run: >
          ./buildtools/checkout-ext-directory.sh grist-ee &&
          docker build -t grist-core:preview . --build-context ext=ext &&
          docker image save grist-core:preview -o grist-core.tar
      - name: Save PR information
        run: |
          echo PR_NUMBER=${{ github.event.number }} >> ./pr-info.txt
          echo PR_SOURCE=${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }} >> ./pr-info.txt
          echo PR_SHASUM=${{ github.event.pull_request.head.sha }} >> ./pr-info.txt
        # PR_SOURCE looks like <owner>/<repo>-<branch>.
        # For example, if the GitHub user "foo" forked grist-core as "grist-bar", and makes a PR from their branch named "baz",
        # it will be "foo/grist-bar-baz". deploy.js later replaces "/" with "-", making it "foo-grist-bar-baz".
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: docker-image
          path: |
            ./grist-core.tar
            ./pr-info.txt
            ./buildtools/fly-template.env
          if-no-files-found: "error"


================================================
FILE: .github/workflows/fly-cleanup.yml
================================================
name: fly.io Cleanup
on:
  schedule:
    # Once a day, clean up jobs marked as expired
    - cron: '50 12 * * *'

  # Allows running this workflow manually from the Actions tab
  workflow_dispatch:

env:
  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

jobs:
  clean:
    name: Clean stale deployed apps
    runs-on: ubuntu-22.04
    if: github.repository_owner == 'gristlabs'
    steps:
      - uses: actions/checkout@v3
      - uses: superfly/flyctl-actions/setup-flyctl@master
        with:
          version: 0.2.72
      - run: node buildtools/fly-deploy.js clean


================================================
FILE: .github/workflows/fly-deploy.yml
================================================
# Follow-up of fly-build, with access to secrets for making deployments.
# This workflow runs in the target repo context. It does not, and should never execute user-supplied code.
# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/

name: fly.io Deploy
on:
  workflow_run:
    workflows: ["fly.io Build"]
    types:
      - completed

jobs:
  deploy:
    name: Deploy app to fly.io
    runs-on: ubuntu-22.04
    if: |
      github.event.workflow_run.event == 'pull_request' &&
      github.event.workflow_run.conclusion == 'success'
    steps:
      - uses: actions/checkout@v4
      - name: Set up flyctl
        uses: superfly/flyctl-actions/setup-flyctl@master
        with:
          version: 0.2.72
      - name: Download artifacts
        uses: actions/github-script@v7
        with:
          script: |
            var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
               owner: context.repo.owner,
               repo: context.repo.repo,
               run_id: ${{ github.event.workflow_run.id }},
            });
            var matchArtifact = artifacts.data.artifacts.filter((artifact) => {
              return artifact.name == "docker-image"
            })[0];
            var download = await github.rest.actions.downloadArtifact({
               owner: context.repo.owner,
               repo: context.repo.repo,
               artifact_id: matchArtifact.id,
               archive_format: 'zip',
            });
            var fs = require('fs');
            fs.writeFileSync('${{github.workspace}}/docker-image.zip', Buffer.from(download.data));
            await github.rest.actions.deleteArtifact({
               owner: context.repo.owner,
               repo: context.repo.repo,
               artifact_id: matchArtifact.id,
            });
      - name: Extract artifacts
        id: extract_artifacts
        run: |
          unzip -o docker-image.zip grist-core.tar pr-info.txt buildtools/fly-template.env
          cat ./pr-info.txt >> $GITHUB_OUTPUT
      - name: Load Docker image
        run: docker load --input grist-core.tar
      - name: Deploy to fly.io
        id: fly_deploy
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
          BRANCH_NAME: ${{ steps.extract_artifacts.outputs.PR_SOURCE }}
        run: |
          node buildtools/fly-deploy.js deploy
          flyctl config -c ./fly.toml env | awk '/APP_HOME_URL/{print "DEPLOY_URL=" $2}' >> $GITHUB_OUTPUT
          flyctl config -c ./fly.toml env | awk '/FLY_DEPLOY_EXPIRATION/{print "EXPIRES=" $2}' >> $GITHUB_OUTPUT
      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: ${{ steps.extract_artifacts.outputs.PR_NUMBER }},
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `Deployed commit \`${{ steps.extract_artifacts.outputs.PR_SHASUM }}\` as ${{ steps.fly_deploy.outputs.DEPLOY_URL }} (until ${{ steps.fly_deploy.outputs.EXPIRES }})`
            })


================================================
FILE: .github/workflows/fly-destroy.yml
================================================
# This workflow runs in the target repo context, as it is triggered via pull_request_target.
# It does not, and should not have access to code in the PR.
# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/

name: fly.io Destroy
on:
  pull_request_target:
    branches: [ main ]
    types: [unlabeled, closed]

  # Allows running this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  destroy:
    name: Remove app from fly.io
    runs-on: ubuntu-22.04
    # Remove the deployment when 'preview' label is removed, or the PR is closed.
    if: |
      github.event_name == 'workflow_dispatch' ||
      (github.event_name == 'pull_request_target' &&
      (github.event.action == 'closed' ||
      (github.event.action == 'unlabeled' && github.event.label.name == 'preview')))
    steps:
      - uses: actions/checkout@v4
      - name: Set up flyctl
        uses: superfly/flyctl-actions/setup-flyctl@master
        with:
          version: 0.2.72
      - name: Destroy fly.io app
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
          BRANCH_NAME: ${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }}
          # See fly-build for what BRANCH_NAME looks like.
        id: fly_destroy
        run: node buildtools/fly-deploy.js destroy


================================================
FILE: .github/workflows/main.yml
================================================
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

  # Allows running this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  build_and_test:
    runs-on: ${{ matrix.os }}
    strategy:
      # it is helpful to know which sets of tests would have succeeded,
      # even when there is a failure.
      fail-fast: false
      matrix:
        os: ['ubuntu-24.04']
        python-version: [3.11]
        node-version: [22.x]
        tests:
          - ':lint:python:client:common:smoke:stubs:pyodide:'
          - ':server-1-of-2:'
          - ':server-2-of-2:'
          - ':gen-server:'
          - ':nbrowser-^[A-D]:'
          - ':nbrowser-^[E-L]:'
          - ':nbrowser-^[M-N]:'
          - ':nbrowser-^[O-R]:'
          - ':nbrowser-^[^A-R]:'
          - ':projects:'
        include:
          - tests: ':lint:python:client:common:smoke:'
            node-version: 22.x
            python-version: '3.10'
            os: ubuntu-24.04
          - tests: ':pyodide:macsandbox:'
            node-version: 22.x
            python-version: '3.11'
            os: macos-latest
    steps:
      - uses: actions/checkout@v3

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'yarn'

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'

      - name: Install Python packages
        run: |
          pip install virtualenv
          yarn run install:python

      - name: Install Node.js packages
        run: yarn install

      - name: Install gvisor
        if: contains(matrix.os, 'ubuntu')
        run: |
          docker create --name temp-runsc gristlabs/gvisor-unprivileged:buster /bin/true
          sudo docker cp temp-runsc:/runsc /usr/bin/runsc
          docker rm temp-runsc

      - name: Run eslint
        if: contains(matrix.tests, ':lint:')
        run: yarn run lint:ci

      - name: Make sure bucket is versioned
        if: contains(matrix.os, 'ubuntu') && contains(matrix.tests, ':server-') || contains(matrix.os, 'ubuntu') && contains(matrix.tests, ':gen-server:')
        env:
          AWS_ACCESS_KEY_ID: administrator
          AWS_SECRET_ACCESS_KEY: administrator
        run: aws --region us-east-1 --endpoint-url http://localhost:9000 s3api put-bucket-versioning --bucket grist-docs-test --versioning-configuration Status=Enabled

      - name: Build Node.js code
        run: yarn run build

      - name: Install Google Chrome and chromedriver
        if: contains(matrix.tests, ':nbrowser-') || contains(matrix.tests, ':smoke:') || contains(matrix.tests, ':stubs:') || contains(matrix.tests, ':projects:')
        run: buildtools/install_chrome_for_tests.sh -y

      - name: Run smoke test
        if: contains(matrix.tests, ':smoke:')
        run: VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:smoke

      - name: Run python tests
        if: contains(matrix.tests, ':python:')
        run: yarn run test:python

      - name: Run client tests
        if: contains(matrix.tests, ':client:')
        run: yarn run test:client

      - name: Run common tests
        if: contains(matrix.tests, ':common:')
        run: yarn run test:common

      - name: Run stubs tests
        if: contains(matrix.tests, ':stubs:')
        run: MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:stubs

      - name: Run gen-server tests with sqlite, minio and redis
        if: contains(matrix.tests, ':gen-server:')
        run: |
          yarn run test:gen-server
        # Anchors should be used once available. Not supported yet as of December 2024.
        # https://github.com/actions/runner/issues/1182
        env:
          MOCHA_WEBDRIVER_HEADLESS: 1
          TESTS: ${{ matrix.tests }}
          GRIST_DOCS_MINIO_ACCESS_KEY: administrator
          GRIST_DOCS_MINIO_SECRET_KEY: administrator
          TEST_REDIS_URL: "redis://localhost/11"
          GRIST_DOCS_MINIO_USE_SSL: 0
          GRIST_DOCS_MINIO_ENDPOINT: localhost
          GRIST_DOCS_MINIO_PORT: 9000
          GRIST_DOCS_MINIO_BUCKET: grist-docs-test

      - name: Run a couple of tests using pyodide
        if: contains(matrix.tests, ':pyodide:')
        run: |
          cd sandbox/pyodide
          make setup
          cd ../..
          yarn run test:server -g 'ActiveDoc.useQuerySet|Sandbox'
          yarn run test:nbrowser -g 'Importer.*should.show.correct.preview'
        env:
          MOCHA_WEBDRIVER_HEADLESS: 1
          GRIST_SANDBOX_FLAVOR: pyodide

      - name: Run a couple of tests using macSandboxExec
        if: contains(matrix.tests, ':macsandbox:')
        run: |
          yarn run test:server -g Sandbox
        env:
          MOCHA_WEBDRIVER_HEADLESS: 1
          GRIST_SANDBOX_FLAVOR: macSandboxExec

      - name: Run gen-server tests with postgres, minio and redis
        if: contains(matrix.tests, ':gen-server:')
        run: |
          PGPASSWORD=$TYPEORM_PASSWORD psql -h $TYPEORM_HOST -U $TYPEORM_USERNAME -w $TYPEORM_DATABASE -c "SHOW ALL;" | grep ' jit '
          yarn run test:gen-server
        env:
          MOCHA_WEBDRIVER_HEADLESS: 1
          TESTS: ${{ matrix.tests }}
          GRIST_DOCS_MINIO_ACCESS_KEY: administrator
          GRIST_DOCS_MINIO_SECRET_KEY: administrator
          TEST_REDIS_URL: "redis://localhost/11"
          GRIST_DOCS_MINIO_USE_SSL: 0
          GRIST_DOCS_MINIO_ENDPOINT: localhost
          GRIST_DOCS_MINIO_PORT: 9000
          GRIST_DOCS_MINIO_BUCKET: grist-docs-test
          TYPEORM_TYPE: postgres
          TYPEORM_HOST: localhost
          TYPEORM_DATABASE: db_name
          TYPEORM_USERNAME: db_user
          TYPEORM_PASSWORD: db_password

      - name: Run server tests with minio and redis
        if: contains(matrix.tests, ':server-')
        run: |
          export TEST_SPLITS=$(echo $TESTS | sed "s/.*:server-\([^:]*\).*/\1/")
          yarn run test:server
        env:
          MOCHA_WEBDRIVER_HEADLESS: 1
          TESTS: ${{ matrix.tests }}
          GRIST_DOCS_MINIO_ACCESS_KEY: administrator
          GRIST_DOCS_MINIO_SECRET_KEY: administrator
          TEST_REDIS_URL: "redis://localhost/11"
          GVISOR_FLAGS: "-unprivileged -ignore-cgroups"
          GVISOR_EXTRA_DIRS: /opt
          GRIST_DOCS_MINIO_USE_SSL: 0
          GRIST_DOCS_MINIO_ENDPOINT: localhost
          GRIST_DOCS_MINIO_PORT: 9000
          GRIST_DOCS_MINIO_BUCKET: grist-docs-test

      - name: Run main tests without minio and redis
        if: contains(matrix.tests, ':nbrowser-')
        run: |
          mkdir -p $MOCHA_WEBDRIVER_LOGDIR
          export GREP_TESTS=$(echo $TESTS | sed "s/.*:nbrowser-\([^:]*\).*/\1/")
          MOCHA_WEBDRIVER_SKIP_CLEANUP=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:nbrowser --parallel --jobs 3
        env:
          TESTS: ${{ matrix.tests }}
          MOCHA_WEBDRIVER_LOGDIR: ${{ runner.temp }}/test-logs/webdriver
          GVISOR_FLAGS: "-unprivileged -ignore-cgroups"
          GVISOR_EXTRA_DIRS: /opt
          TESTDIR: ${{ runner.temp }}/test-logs

      - name: Run projects tests
        if: contains(matrix.tests, ':projects:')
        run: |
          mkdir -p $MOCHA_WEBDRIVER_LOGDIR
          yarn run test:projects
        env:
          MOCHA_WEBDRIVER_LOGDIR: ${{ runner.temp }}/test-logs/webdriver
          TESTDIR: ${{ runner.temp }}/test-logs
          MOCHA_WEBDRIVER_HEADLESS: 1

      - name: Prepare for saving artifact
        if: failure()
        run: |
          ARTIFACT_NAME=logs-$(echo $TESTS | sed 's/[^-a-zA-Z0-9]/_/g')
          echo "Artifact name is '$ARTIFACT_NAME'"
          echo "ARTIFACT_NAME=$ARTIFACT_NAME" >> $GITHUB_ENV
          mkdir -p $TESTDIR
          find $TESTDIR -iname "*.socket" -exec rm {} \;
        env:
          TESTS: ${{ matrix.tests }}
          TESTDIR: ${{ runner.temp }}/test-logs

      - name: Save artifacts on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: ${{ env.ARTIFACT_NAME }}
          path: ${{ runner.temp }}/test-logs  # only exists for webdriver tests

    services:
      # https://github.com/bitnami/containers/issues/83267
      minio:
        image: ${{ matrix.os == 'ubuntu-24.04' && 'bitnamilegacy/minio:2025.4.22' || '' }} 
        env:
          MINIO_DEFAULT_BUCKETS: "grist-docs-test:public"
          MINIO_ROOT_USER: administrator
          MINIO_ROOT_PASSWORD: administrator
        ports:
          - 9000:9000
        options: >-
          --health-cmd "curl -f http://localhost:9000/minio/health/ready"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      redis:
        image: ${{ matrix.os == 'ubuntu-24.04' && 'redis' || '' }}
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      postgresql:
        image: ${{ matrix.os == 'ubuntu-24.04' && 'postgres:latest' || '' }}
        env:
          POSTGRES_USER: db_user
          POSTGRES_PASSWORD: db_password
          POSTGRES_DB: db_name
          # JIT is enabled by default since Postgres 17 and has a huge negative impact on performance,
          # making many tests timeout.
          # https://support.getgrist.com/self-managed/#what-is-a-home-database
          POSTGRES_INITDB_ARGS: "-c jit=off"
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U db_user"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

  candidate:
    needs: build_and_test
    if: ${{ success() && github.event_name == 'push' }}
    runs-on: ubuntu-22.04
    steps:
      - name: Fetch new candidate branch
        uses: actions/checkout@v3

      - name: Update candidate branch
        uses: ad-m/github-push-action@8407731efefc0d8f72af254c74276b7a90be36e1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          branch: latest_candidate
          force: true


================================================
FILE: .github/workflows/self-hosted.yml
================================================
name: Add self-hosting issues to the self-hosting project

on:
  issues:
    types:
      - opened
      - labeled

jobs:
  add-to-project:
    name: Add issue to project
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/add-to-project@v1.0.1
        with:
          project-url: https://github.com/orgs/gristlabs/projects/2
          github-token: ${{ secrets.SELF_HOSTED_PROJECT }}
          labeled: self-hosting


================================================
FILE: .github/workflows/translation_keys.yml
================================================
name: Translation keys

on:
  push:
    branches: [ main ]
  workflow_dispatch:

permissions:
  pull-requests: write
  contents: write

jobs:
  build:
    if: github.repository_owner == 'gristlabs'
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0 # Let's get all the branches

      - name: Use Node.js
        uses: actions/setup-node@v1
        with:
          node-version: 22

      - name: Install Node.js packages
        run: yarn install

      - name: Build code
        run: yarn run build

      - name: Scan for keys
        id: scan-keys
        run: |
          git checkout -b translation-keys
          yarn run generate:translation 2>&1 | tee /tmp/scan-output.txt
          git status --porcelain
          if [[ $(git status --porcelain | wc -l) -eq "0" ]]; then
            echo "No changes"
            echo "CHANGED=false" >> $GITHUB_ENV
          else
            echo "Changes detected"
            echo "CHANGED=true" >> $GITHUB_ENV
          fi

      - name: setup git config
        run: |
          git config user.name "Paul's Grist Bot"
          git config user.email "<paul+bot@getgrist.com>"

      - name: Create PR and merge
        if: env.CHANGED == 'true'
        run: |
          git commit -m "automated update to translation keys" -a
          git push --set-upstream origin HEAD:translation-keys -f
          num=$(gh pr list --search "automated update to translation keys" --json number -q ".[].number")
          if [[ "$num" != "" ]]; then
            echo "Existing translation keys PR #$num is open, skipping"
            exit 0
          fi
          sed -n '/TRANSLATION_SUMMARY_START/,/TRANSLATION_SUMMARY_END/{//d;p;}' /tmp/scan-output.txt > /tmp/pr-body.txt
          gh pr create --title "automated update to translation keys" --body-file /tmp/pr-body.txt
          gh pr merge --merge --delete-branch
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
/node_modules/
/_build/
/static/*.bundle.js
/static/*.bundle.js.map
/static/grist-plugin-api*
/static/bundle.css
/static/browser-check.js
/static/*.bundle.js.*.txt
/grist-sessions.db
/landing.db
/docs/
/sandbox_venv*
/.vscode/

# Files created by grist-desktop setup
/cpython.tar.gz
/python
/static_ext

# Build helper files.
/.build*

*.swp
*.pyc
*.bak
.DS_Store

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
/node_modules/
jspm_packages/
/docs

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# Test
timings.txt
xunit.xml
.clipboard.lock

**/_build

# ext directory can be overwritten
/ext
/ext/**

# Docker compose examples - persistent values and secrets
/docker-compose-examples/*/persist
/docker-compose-examples/*/secrets
/docker-compose-examples/grist-traefik-oidc-auth/.env

# Sample grist documents
/samples/

/test/assistant/data/cache/
/test/assistant/data/results/
/test/assistant/data/templates/


================================================
FILE: .nvmrc
================================================
v22.12.0


================================================
FILE: .yarnrc
================================================
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


yarn-offline-mirror false


================================================
FILE: CONTRIBUTING.md
================================================
# Welcome to the contribution guide for Grist!

You are eager to contribute to Grist? That's awesome! See below some contributions you can make:
- [translate](/documentation/translations.md)
- [write tutorials and user documentation](https://github.com/gristlabs/grist-help?tab=readme-ov-file#grist-help-center)
- [develop](/documentation/develop.md)
- [report issues or suggest enhancement](https://github.com/gristlabs/grist-core/issues/new/choose)



================================================
FILE: Dockerfile
================================================
################################################################################
## The Grist source can be extended. This is a stub that can be overridden
## from command line, as:
##   docker buildx build -t ... --build-context=ext=<path> .
## The code in <path> will then be built along with the rest of Grist.
################################################################################
FROM scratch AS ext

################################################################################
## Javascript build stage
################################################################################

FROM node:22-trixie AS prod-builder

# Install all node dependencies.
WORKDIR /grist
COPY package.json yarn.lock /grist/
RUN \
  yarn install --prod --frozen-lockfile --verbose --network-timeout 600000

FROM prod-builder AS builder

# Create node_modules with devDependencies to be able to build the app
# Add at global level gyp deps to build sqlite3 for prod
# then create node_modules_prod that will be the node_modules of final image
RUN \
  yarn install --frozen-lockfile --verbose --network-timeout 600000

# Install any extra node dependencies (at root level, to avoid having to wrestle
# with merging them).
COPY --from=ext / /grist/ext
RUN \
 mkdir /node_modules && \
 cd /grist/ext && \
 { if [ -e package.json ] ; then yarn install --frozen-lockfile --modules-folder=/node_modules --verbose --network-timeout 600000 ; fi }

# Build node code.
COPY tsconfig.json /grist
COPY tsconfig-ext.json /grist
COPY tsconfig-prod.json /grist
COPY test/tsconfig.json /grist/test/tsconfig.json
COPY test/chai-as-promised.js /grist/test/chai-as-promised.js
COPY app /grist/app
COPY stubs /grist/stubs
COPY buildtools /grist/buildtools
# Copy locales files early. During build process they are validated.
COPY static/locales /grist/static/locales
RUN WEBPACK_EXTRA_MODULE_PATHS=/node_modules yarn run build:prod
# We don't need them anymore, they will by copied to the final image.
RUN rm -rf /grist/static/locales


# Prepare material for optional pyodide sandbox
COPY sandbox/pyodide /grist/sandbox/pyodide
COPY sandbox/requirements.txt /grist/sandbox/requirements.txt
RUN \
  cd /grist/sandbox/pyodide && make setup

################################################################################
## Python collection stage
################################################################################

# Fetch python3.11
FROM python:3.11-slim-trixie AS collector-py3
COPY sandbox/requirements.txt requirements.txt
RUN \
  pip3 install -r requirements.txt

################################################################################
## Sandbox collection stage
################################################################################

# Fetch gvisor-based sandbox. Note, to enable it to run within default
# unprivileged docker, layers of protection that require privilege have
# been stripped away, see https://github.com/google/gvisor/issues/4371
# The standalone sandbox binary is built on buster, but remains compatible
# with recent Debian.
# If you'd like to use unmodified gvisor, you should be able to just drop
# in the standard runsc binary and run the container with any extra permissions
# it needs.
FROM docker.io/gristlabs/gvisor-unprivileged:buster AS sandbox

################################################################################
## Run-time stage
################################################################################

# Now, start preparing final image.
FROM node:22-trixie-slim

ARG GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING=false

# Install curl for docker healthchecks, libexpat1 and libsqlite3-0 for python3
# library binary dependencies, and procps for managing gvisor processes.
RUN \
  apt-get update && \
  apt-get install -y --no-install-recommends curl libexpat1 libsqlite3-0 procps tini && \
  rm -rf /var/lib/apt/lists/*

# Keep all storage user may want to persist in a distinct directory
RUN mkdir -p /persist/docs

# Copy node files.
COPY --from=builder /node_modules /node_modules
COPY --from=prod-builder /grist/node_modules /grist/node_modules
COPY --from=builder /grist/_build /grist/_build
COPY --from=builder /grist/static /grist/static-built
COPY --from=builder /grist/app/cli.sh /grist/cli
# Patterm match here is to copy assets only if it exists in the
# builder stage, otherwise matches nothing.
# https://stackoverflow.com/a/70096420/11352427
COPY --from=builder /grist/ext/asset[s] /grist/ext/assets

# Copy python3 files.
COPY --from=collector-py3 /usr/local/bin/python3.11 /usr/bin/python3.11
COPY --from=collector-py3 /usr/local/lib/python3.11 /usr/local/lib/python3.11
COPY --from=collector-py3 /usr/local/lib/libpython3.11.* /usr/local/lib/
# Set default to python3
RUN \
  ln -s /usr/bin/python3.11 /usr/bin/python && \
  ln -s /usr/bin/python3.11 /usr/bin/python3 && \
  ldconfig

# Copy runsc.
COPY --from=sandbox /runsc /usr/bin/runsc

# Add files needed for running server.
COPY package.json /grist/package.json
COPY bower_components /grist/bower_components
COPY sandbox /grist/sandbox
COPY plugins /grist/plugins
COPY static /grist/static

# Make optional pyodide sandbox available
COPY --from=builder /grist/sandbox/pyodide /grist/sandbox/pyodide

# Finalize static directory
RUN \
  mv /grist/static-built/* /grist/static && \
  rmdir /grist/static-built

# To ensure non-root users can run grist, 'other' users need read access (and execute on directories)
# This should be the case by default when copying files in.
# Only uncomment this if running into permissions issues, as it takes a long time to execute on some systems.
# RUN chmod -R o+rX /grist

# Add a user to allow de-escalating from root on startup
RUN useradd -ms /bin/bash grist
ENV GRIST_DOCKER_USER=grist \
    GRIST_DOCKER_GROUP=grist
WORKDIR /grist

# Set some default environment variables to give a setup that works out of the box when
# started as:
#   docker run -p 8484:8484 -it <image>
# Variables will need to be overridden for other setups.
#
# GRIST_SANDBOX_FLAVOR is set to unsandboxed by default, because it
# appears that the services people use to run docker containers have
# a wide variety of security settings and the functionality needed for
# sandboxing may not be possible in every case. For default docker
# settings, you can get sandboxing as follows:
#   docker run --env GRIST_SANDBOX_FLAVOR=gvisor -p 8484:8484 -it <image>
#
# "NODE_OPTIONS=--no-deprecation" is set because there is a punycode
# deprecation nag that is relevant to developers but not to users.
# TODO: upgrade package.json to avoid using all package versions
# using the punycode functionality that may be removed in future
# versions of node.
#
# "NODE_ENV=production" gives ActiveDoc operations more time to
# complete, and the express webserver also does some streamlining
# with this setting. If you don't want these, set NODE_ENV to
# development.
#
ENV \
  GRIST_ORG_IN_PATH=true \
  GRIST_HOST=0.0.0.0 \
  GRIST_SINGLE_PORT=true \
  GRIST_SERVE_SAME_ORIGIN=true \
  GRIST_DATA_DIR=/persist/docs \
  GRIST_INST_DIR=/persist \
  GRIST_SESSION_COOKIE=grist_core \
  GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING=${GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING} \
  GVISOR_FLAGS="-unprivileged -ignore-cgroups" \
  GRIST_SANDBOX_FLAVOR=unsandboxed \
  NODE_OPTIONS="--no-deprecation" \
  NODE_ENV=production \
  TYPEORM_DATABASE=/persist/home.sqlite3

EXPOSE 8484

# When run without any arguments, we run the Grist server within
# a simple supervisor.
# When arguments are supplied they are treated as a command to run,
# as is default for docker. We arrange to have a "cli" command that
# is the same as "yarn cli" run from the source code repo.
# So you can do things like:
# docker run --rm -v $PWD:$PWD -it gristlabs/grist \
#   cli sqlite query $PWD/docs/4gtUhAEGbGAdsGNc52k4H6.grist \
#  --json "select * from _gristsys_ActionHistory"

ENTRYPOINT ["./sandbox/docker_entrypoint.sh"]
CMD ["node", "./sandbox/supervisor.mjs"]


================================================
FILE: LICENSE.txt
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2014-2022 Grist Labs Inc.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: NOTICE.txt
================================================
Grist Software
Copyright 2014-2022 Grist Labs Inc.

This product includes software developed at
Grist Labs Inc. (https://www.getgrist.com/).


================================================
FILE: README.md
================================================
# Grist

Grist is a modern relational spreadsheet. It combines the flexibility of a spreadsheet with the robustness of a database.

* `grist-core` (this repo) has what you need to run a powerful server for hosting spreadsheets.

* [`grist-desktop`](https://github.com/gristlabs/grist-desktop) is a Linux/macOS/Windows desktop app for viewing and editing spreadsheets stored locally.
* [`grist-static`](https://github.com/gristlabs/grist-static) is a fully in-browser build of Grist for displaying spreadsheets on a website without back-end support.

Grist is developed by [Grist Labs](https://www.linkedin.com/company/grist-labs/), an NYC-based company 🇺🇸🗽. The French government 🇫🇷 organizations [ANCT Données et Territoires](https://donnees.incubateur.anct.gouv.fr/toolbox/grist) and [DINUM (Direction Interministérielle du Numérique)](https://www.numerique.gouv.fr/dinum/) have also made significant contributions to the codebase.

The `grist-core`, `grist-desktop`, and `grist-static` repositories are all open source (Apache License, Version 2.0).
Grist Labs offers free and paid hosted services at [getgrist.com](https://getgrist.com), sells an Enterprise product,
and offers [cloud packaging](https://support.getgrist.com/install/grist-builder-edition/).

> Questions? Feedback? Want to share what you're building with Grist? Join our [official Discord server](https://discord.gg/MYKpYQ3fbP) or visit our [Community forum](https://community.getgrist.com/). 
>
> To keep up-to-date with everything that's going on, you can [sign up for Grist's monthly newsletter](https://www.getgrist.com/newsletter/).

https://github.com/user-attachments/assets/fe152f60-3d15-4b11-8cb2-05731a90d273

## Features in `grist-core`

To see exactly what is present in `grist-core`, you can run the [desktop app](https://github.com/gristlabs/grist-desktop), or use [`docker`](#using-grist). The absolute fastest way to try Grist out is to visit [docs.getgrist.com](https://docs.getgrist.com) and play with a spreadsheet there immediately – though if you do, please read the list of [extra extensions](#features-not-in-grist-core) that are not in `grist-core`.

However you try it, you'll quickly see that Grist is a hybrid database/spreadsheet, meaning that:

  - Columns work like they do in databases: they are named, and they hold one kind of data.
  - Columns can be filled by formula, spreadsheet-style, with automatic updates when referenced cells change.

This difference can confuse people coming directly from Excel or Google Sheets. Give it a chance! There's also a [Grist for Spreadsheet Users](https://www.getgrist.com/blog/grist-for-spreadsheet-users/) article to help get you oriented. If you're coming from Airtable, you'll find the model familiar (and there's also our [Grist vs Airtable](https://www.getgrist.com/blog/grist-v-airtable/) article for a direct comparison).

Here are some specific feature highlights of Grist:

  * Python formulas.
    - Full [Python syntax is supported](https://support.getgrist.com/formulas/#python), including the standard library.
    - Many [Excel functions](https://support.getgrist.com/functions/) also available.
    - An [AI Assistant](https://www.getgrist.com/ai-formula-assistant/) specifically tuned for formula generation (using OpenAI gpt-3.5-turbo or [Llama](https://ai.meta.com/llama/) via <a href="https://github.com/abetlen/llama-cpp-python">llama-cpp-python</a>).
  * A portable, self-contained format.
    - Based on SQLite, the most widely deployed database engine.
    - Any tool that can read SQLite can read numeric and text data from a Grist file.
    - Enables [backups](https://support.getgrist.com/exports/#backing-up-an-entire-document) that you can confidently restore in full.
    - Great for moving between different hosts.
  * Can be displayed on a static website with [`grist-static`](https://github.com/gristlabs/grist-static) – no special server needed.
  * A self-contained desktop app for viewing and editing locally: [`grist-desktop`](https://github.com/gristlabs/grist-desktop).
  * Convenient editing and formatting features.
    - Choices and [choice lists](https://support.getgrist.com/col-types/#choice-list-columns), for adding colorful tags to records.
    - [References](https://support.getgrist.com/col-refs/#creating-a-new-reference-list-column) and reference lists, for cross-referencing records in other tables.
    - [Attachments](https://support.getgrist.com/col-types/#attachment-columns), to include media or document files in records.
    - Dates and times, toggles, and special numerics such as currency all have specialized editors and formatting options.
    - [Conditional Formatting](https://support.getgrist.com/conditional-formatting/), letting you control the style of cells with formulas to draw attention to important information.
  * Drag-and-drop dashboards.
    - [Charts](https://support.getgrist.com/widget-chart/), [card views](https://support.getgrist.com/widget-card/) and a [calendar widget](https://support.getgrist.com/widget-calendar/) for visualization.
    - [Summary tables](https://support.getgrist.com/summary-tables/) for summing and counting across groups.
    - [Widget linking](https://support.getgrist.com/linking-widgets/) streamlines filtering and editing data.
    Grist has a unique approach to visualization, where you can lay out and link distinct widgets to show together,
    without cramming mixed material into a table.
    - [Filter bar](https://support.getgrist.com/search-sort-filter/#filter-buttons) for quick slicing and dicing.
  * [Incremental imports](https://support.getgrist.com/imports/#updating-existing-records).
    - Import a CSV of the last three months activity from your bank...
    - ...and import new activity a month later without fuss or duplication.
  * [Native forms](https://support.getgrist.com/widget-form/). Create forms that feed directly into your spreadsheet without fuss.
  * Integrations.
    - A [REST API](https://support.getgrist.com/api/), [Zapier actions/triggers](https://support.getgrist.com/integrators/#integrations-via-zapier), and support from similar [integrators](https://support.getgrist.com/integrators/).
    - Import/export to Google drive, Excel format, CSV.
    - Link data with [custom widgets](https://support.getgrist.com/widget-custom/#_top), hosted externally.
    - Configurable outgoing webhooks.
  * [Many templates](https://templates.getgrist.com/) to get you started, from investment research to organizing treasure hunts.
  * Access control options.
    - (You'll need SSO logins set up to make use of these options; [`grist-omnibus`](https://github.com/gristlabs/grist-omnibus) has a prepackaged solution if configuring this feels daunting)
    - Share [individual documents](https://support.getgrist.com/sharing/), workspaces, or [team sites](https://support.getgrist.com/team-sharing/).
    - Control access to [individual rows, columns, and tables](https://support.getgrist.com/access-rules/).
    - Control access based on cell values and user attributes.
  * Self-maintainable.
    - Useful for intranet operation and specific compliance requirements.
  * Sandboxing options for untrusted documents.
    - On Linux or with Docker, you can enable [gVisor](https://github.com/google/gvisor) sandboxing at the individual document level.
    - On macOS, you can use native sandboxing.
    - On any OS, including Windows, you can use a wasm-based sandbox.
  * Translated to many languages.
  * `F1` key brings up some quick help. This used to go without saying, but in general Grist has good keyboard support.
  * We post progress on [𝕏 or Twitter or whatever](https://twitter.com/getgrist) and publish [monthly newsletters](https://support.getgrist.com/newsletters/).

If you are curious about where Grist is heading, see [our roadmap](https://github.com/gristlabs/grist-core/projects/1), drop a question in [our forum](https://community.getgrist.com), or browse [our extensive documentation](https://support.getgrist.com).

## Features not in `grist-core`

If you evaluate Grist by using the hosted version at [getgrist.com](https://getgrist.com), be aware that it includes some extensions to Grist that aren't present in `grist-core`. To be sure you're seeing exactly what is present in `grist-core`, you can run the [desktop app](https://github.com/gristlabs/grist-desktop), or use [`docker`](#using-grist). Here is a list of features you may see in Grist Labs' hosting or Enterprise offerings that are not in `grist-core`, in chronological order of creation. If self-hosting, you can get access to a free trial of all of them using the Enterprise toggle on the [Admin Panel](https://support.getgrist.com/admin-panel/).

  * [GristConnect](https://support.getgrist.com/install/grist-connect/) (2022)
    - Any site that has plugins for letting Discourse use its logins (such as WordPress) can also let Grist use its logins.
    - GristConnect is a niche feature built for a specific client which you probably don't care about – `OIDC` and `SAML` support *is* part of `grist-core` and covers most authentication use cases.
  * [Azure back-end for document storage](https://support.getgrist.com/install/cloud-storage/#azure) (2022)
    - With `grist-core` you can store document versions in anything S3-compatible, which covers a lot of services, but not Azure specifically. The Azure back-end fills that gap.
    - Unless you are a Microsoft shop you probably don't care about this.
  * [Audit log streaming](https://support.getgrist.com/install/audit-log-streaming/) (2024)
    - With `grist-core` a lot of useful information is logged, but not organized specifically with auditing in mind. Audit log streaming supplies that organization, and a UI for setting things up.
    - Enterprises may care about this.
  * [Advanced Admin Controls](https://support.getgrist.com/admin-controls/) (2025)
    - This is a special page for a Grist installation administrator to monitor and edit user access to resources.
    - It uses a special set of administrative endpoints not present on `grist-core`.
    - If you're going to be running a large Grist installation, with employees coming and going, you may care about this.
  * [Grist Assistant](https://support.getgrist.com/assistant/#assistant) (2025)
    - An AI Formula Assistant - limited to working with formulas - is present in `grist-core`, but the newer Assistant can help with a wider range of tasks like building tables and dashboards and modifying data.
    - If you have many users who need help building documents or working with data, you may care about this one.
  * [Invite Notifications](https://support.getgrist.com/self-managed/#how-do-i-set-up-email-notifications) (2025)
    - When a user is added to a document, or a workspace, or a site, with email notifications they will get emailed a link to access the resource.
    - This link isn't special, with `grist-core` you can just send a link yourself or a colleague.
    - For a big Grist installation with users who aren't in close communication, emails might be nice? Hard to guess if you'll care about this one.
  * [Document Change and Comment Notifications](https://support.getgrist.com/document-settings/#notifications) (2025)
    - You can achieve change notifications in `grist-core` using webhooks, but it is less convenient.
    - People have been asking for this one for years. If you need an excuse to get your boss to pay for Grist, this might finally be the one that works?

## Using Grist

To get the default version of `grist-core` running on your computer
with [Docker](https://www.docker.com/get-started), do:

```sh
docker pull gristlabs/grist
docker run -p 8484:8484 -it gristlabs/grist
```

Then visit `http://localhost:8484` in your browser. You'll be able to create, edit, import,
and export documents. To preserve your work across docker runs, share a directory as `/persist`:

```sh
docker run -p 8484:8484 -v $PWD/persist:/persist -it gristlabs/grist
```

Get templates at [templates.getgrist.com](https://templates.getgrist.com) for payroll,
inventory management, invoicing, D&D encounter tracking, and a lot
more, or use any document you've created on
[docs.getgrist.com](https://docs.getgrist.com).

If you need to change the port Grist runs on, set a `PORT` variable, don't just change the
port mapping:

```
docker run --env PORT=9999 -p 9999:9999 -v $PWD/persist:/persist -it gristlabs/grist
```

To enable gVisor sandboxing, set `--env GRIST_SANDBOX_FLAVOR=gvisor`.
This should work with default docker settings, but may not work in all
environments.

You can find a lot more about configuring Grist, setting up authentication,
and running it on a public server in our
[Self-Managed Grist](https://support.getgrist.com/self-managed/) handbook.

## Using Grist with OpenRouter for Model Agnostic and Claude Support

(Instructions contributed by @lshalon)

Grist's AI Formula Assistant can be configured to use OpenRouter instead of connecting directly to OpenAI, allowing you to access a wide range of AI models including Anthropic's Claude models. This isn't the only way to use Claude models, but it's a good option if you want to use Claude models with Grist or intend to use other cheaper, faster, or potentially newer models. That's because this configuration gives you more flexibility in choosing the AI model that works best for your formula generation needs.
To set up OpenRouter integration, configure the following environment variables:

### Required: Set the endpoint to OpenRouter's API

```
ASSISTANT_CHAT_COMPLETION_ENDPOINT=https://openrouter.ai/api/v1/chat/completions
```

### Required: Your OpenRouter API key

```
ASSISTANT_API_KEY=your_openrouter_api_key_here
```

Sign up for an OpenRouter API key at <https://openrouter.ai/>

### Optional: Specify which model to use (examples below)

```
ASSISTANT_MODEL=anthropic/claude-3.7-sonnet
```

### or other options like

```
ASSISTANT_MODEL=deepseek/deepseek-r1-zero:free
```

```
ASSISTANT_MODEL=qwen/qwq-32b:free
```

```
ASSISTANT_MODEL=mistralai/mistral-saba
```

### Optional: Set a larger context model for fallback

```
ASSISTANT_LONGER_CONTEXT_MODEL=anthropic/claude-3-opus-20240229
```

With this configuration, Grist's AI Formula Assistant will route requests through OpenRouter to your specified model. This allows you to:

Access Anthropic's Claude models which excel at understanding context and generating accurate formulas
Switch between different AI models without changing your Grist configuration
Take advantage of OpenRouter's routing capabilities to optimize for cost, speed, or quality

You can find the available models and their identifiers on the OpenRouter website.
Note: Make sure not to set the OPENAI_API_KEY variable when using OpenRouter, as this would override the OpenRouter configuration.


## Available Docker images

The default Docker image is `gristlabs/grist`. This contains all of
the standard Grist functionality, as well as extra source-available
code for enterprise customers taken from the
[grist-ee](https://github.com/gristlabs/grist-ee) repository. This
extra code is not under a free or open source license. By default,
however, the code from the `grist-ee` repository is completely inert
and inactive. This code becomes active only when enabled from the
administrator panel.

If you would rather use an image that contains exclusively free and
open source code, the `gristlabs/grist-oss` Docker image is available
for this purpose. It is by default functionally equivalent to the
`gristlabs/grist` image.

## The administrator panel

You can turn on a special admininistrator panel to inspect the status
of your installation. Just visit `/admin` on your Grist server for
instructions. Since it is useful for the admin panel to be
available even when authentication isn't set up, you can give it a
special access key by setting `GRIST_BOOT_KEY`.

```
docker run -p 8484:8484 -e GRIST_BOOT_KEY=secret -it gristlabs/grist
```

The boot page should then be available at
`/admin?boot-key=<GRIST_BOOT_KEY>`. We are collecting probes for
common problems there. If you hit a problem that isn't covered, it
would be great if you could add a probe for it in
[BootProbes](https://github.com/gristlabs/grist-core/blob/main/app/server/lib/BootProbes.ts).
You may instead file an issue so someone else can add it.

## Building from source

To build Grist from source, follow these steps:

    yarn install
    yarn install:python
    yarn build
    yarn start
    # Grist will be available at http://localhost:8484/

Grist formulas in documents will be run using Python executed directly on your
machine. You can configure sandboxing using a `GRIST_SANDBOX_FLAVOR`
environment variable.

 * On macOS, `export GRIST_SANDBOX_FLAVOR=macSandboxExec`
   uses the native `sandbox-exec` command for sandboxing.
 * On Linux with [gVisor's runsc](https://github.com/google/gvisor)
   installed, `export GRIST_SANDBOX_FLAVOR=gvisor` is an option.
 * On any OS including Windows, `export GRIST_SANDBOX_FLAVOR=pyodide` is available.

These sandboxing methods have been written for our own use at Grist Labs and
may need tweaking to work in your own environment - pull requests
very welcome here!

If you wish to include Grist Labs enterprise extensions in your build,
the steps are as follows. Note that this will add non-OSS code to your
build. It will also place a directory called `node_modules` one level
up, at the same level as the Grist repo. If that is a problem for you,
just move everything into a subdirectory first.

    yarn install
    yarn install:ee
    yarn install:python
    yarn build
    yarn start
    # Grist will be available at http://localhost:8484/

The enterprise code will by default not be used. You need to explicitly enable
it in the [Admin Panel](https://support.getgrist.com/self-managed/#how-do-i-enable-grist-enterprise).

## Logins

Like git, Grist has features to track document revision history. So for full operation,
Grist expects to know who the user modifying a document is. Until it does, it operates
in a limited anonymous mode. To get you going, the docker image is configured so that
when you click on the "sign in" button Grist will attribute your work to `you@example.com`.
Change this by setting `GRIST_DEFAULT_EMAIL`:

```
docker run --env GRIST_DEFAULT_EMAIL=my@email -p 8484:8484 -v $PWD/persist:/persist -it gristlabs/grist
```

You can change your name in `Profile Settings` in
the [User Menu](https://support.getgrist.com/glossary/#user-menu).

For multi-user operation, or if you wish to access Grist across the
public internet, you'll want to connect it to your own Single Sign-On service.
There are a lot of ways to do this, including [SAML and forward authentication](https://support.getgrist.com/self-managed/#how-do-i-set-up-authentication).
Grist has been tested with [Authentik](https://goauthentik.io/), [Auth0](https://auth0.com/),
and Google/Microsoft sign-ins via [Dex](https://dexidp.io/).

## Translations

We use [Weblate](https://hosted.weblate.org/engage/grist/) to manage translations.
Thanks to everyone who is pitching in. Thanks especially to the ANCT developers who
did the hard work of making a good chunk of the application localizable. Merci beaucoup !

<a href="https://hosted.weblate.org/engage/grist/">
<img src="https://hosted.weblate.org/widgets/grist/-/open-graph.png" alt="Translation status" width=480 />
</a>

[![Translation detail](https://hosted.weblate.org/widgets/grist/-/multi-green.svg)](https://hosted.weblate.org/engage/grist/)

## Why free and open source software

This repository, `grist-core`, is maintained by Grist Labs. Our flagship product available at [getgrist.com](https://www.getgrist.com) is built from the code you see here, combined with business-specific software designed to scale to many users, handle billing, etc.

Grist Labs is an open-core company. We offer Grist hosting as a service, with free and paid plans. We also develop and sell features related to Grist using a proprietary license, targeted at the needs of enterprises with large self-managed installations.

We see data portability and autonomy as a key value, and `grist-core` is an essential part of that. We are committed to maintaining and improving the `grist-core` codebase, and to be thoughtful about how proprietary offerings impact data portability and autonomy.

By opening its source code and offering an [OSI](https://opensource.org/)-approved free license, Grist benefits its users:

- **Developer community.** The freedom to examine source code, make bug fixes, and develop
  new features is a big deal for a general-purpose spreadsheet-like product, where there is a
  very long tail of features vital to someone somewhere.
- **Increased trust.** Because anyone can examine the source code, &ldquo;security by obscurity&rdquo; is not
  an option. Vulnerabilities in the code can be found by others and reported before they cause
  damage.
- **Independence.** Grist is available to you regardless of the fortunes of the Grist Labs business,
  since it is open source and can be self-hosted. Using our hosted solution is convenient, but you
  are not locked in.
- **Price flexibility.** If you are low on funds but have time to invest, self-hosting is a great
  option to have. And DIY users may have the technical savvy and motivation to delve in and make improvements,
  which can benefit all users of Grist.
- **Extensibility.** For developers, having the source open makes it easier to build extensions (such as [Custom Widgets](https://support.getgrist.com/widget-custom/)). You can more easily include Grist in your pipeline. And if a feature is missing, you can just take the source code and build on top of it.

For more on Grist Labs' history and principles, see our [About Us](https://www.getgrist.com/about/) page.

## Sponsors

<p align="center">
  <a href="https://www.dotphoton.com/">
    <img width="11%" src="https://user-images.githubusercontent.com/11277225/228914729-ae581352-b37a-4ca8-b220-b1463dd1ade0.png" />
  </a>
</p>

## Reviews

 * [Grist on ProductHunt](https://www.producthunt.com/posts/grist-2)
 * [Grist on AppSumo](https://appsumo.com/products/grist/) (life-time deal is sold out)
 * [Capterra](https://www.capterra.com/p/232821/Grist/#reviews), [G2](https://www.g2.com/products/grist/reviews), [TrustRadius](https://www.trustradius.com/products/grist/reviews)

## Environment variables

Grist can be configured in many ways. Here are the main environment variables it is sensitive to:

| Variable | Purpose |
| -------- | ------- |
| ALLOWED_WEBHOOK_DOMAINS | comma-separated list of permitted domains to use in webhooks (e.g. webhook.site,zapier.com). You can set this to `*` to allow all domains, but if doing so, we recommend using a carefully locked-down proxy (see `GRIST_PROXY_FOR_UNTRUSTED_URLS`) if you do not entirely trust users. Otherwise services on your internal network may become vulnerable to manipulation. |
| APP_DOC_URL | doc worker url, set when starting an individual doc worker (other servers will find doc worker urls via redis) |
| APP_DOC_INTERNAL_URL | like `APP_DOC_URL` but used by the home server to reach the server using an internal domain name resolution (like in a docker environment). It only makes sense to define this value in the doc worker. Defaults to `APP_DOC_URL`. |
| APP_HOME_URL | url prefix for home api (home and doc servers need this) |
| APP_HOME_INTERNAL_URL | like `APP_HOME_URL` but used by the home and the doc servers to reach any home workers using an internal domain name resolution (like in a docker environment). Defaults to `APP_HOME_URL` |
| APP_STATIC_URL | url prefix for static resources |
| APP_STATIC_INCLUDE_CUSTOM_CSS | set to "true" to include custom.css (from APP_STATIC_URL) in static pages |
| APP_UNTRUSTED_URL | URL at which to serve/expect plugin content. |
| GRIST_ACTION_HISTORY_MAX_ROWS | Maximum number of rows allowed in ActionHistory before pruning (up to a 1.25 grace factor). Defaults to 1000. ⚠️ A too low value may make the "[Work on a copy](https://support.getgrist.com/newsletters/2021-06/#work-on-a-copy)" feature [malfunction](https://github.com/gristlabs/grist-core/issues/1121#issuecomment-2248112023) |
| GRIST_ACTION_HISTORY_MAX_BYTES | Maximum number of rows allowed in ActionHistory before pruning (up to a 1.25 grace factor). Defaults to 1Gb. ⚠️ A too low value may make the "[Work on a copy](https://support.getgrist.com/newsletters/2021-06/#work-on-a-copy)" feature [malfunction](https://github.com/gristlabs/grist-core/issues/1121#issuecomment-2248112023) |
| GRIST_ADAPT_DOMAIN | set to "true" to support multiple base domains (careful, host header should be trustworthy) |
| GRIST_ALLOW_AUTOMATIC_VERSION_CHECKING | Whether Grist is allowed to automatically check if a newer Grist version is available. Defaults to "true" on the default `grist` and `grist-ee` Docker images. Defaults false in `grist-oss` and everywhere else. |
| GRIST_ALLOW_DEPRECATED_BARE_ORG_DELETE | If set, the deprecated DELETE /api/orgs/:orgId endpoint is available. |
| GRIST_APP_ROOT | directory containing Grist sandbox and assets (specifically the sandbox and static subdirectories). |
| GRIST_ATTACHMENT_THRESHOLD_MB | attachment storage limit per document beyond which Grist will recommend external storage (if available). Defaults to 50MB. |
| GRIST_BACKUP_DELAY_SECS | wait this long after a doc change before making a backup |
| GRIST_BOOT_KEY | if set, offer diagnostics at /boot/GRIST_BOOT_KEY |
| GRIST_BROADCAST_TIMEOUT_MS | Set the maximum time a web client has to accept a broadcast message about a document before being disconnected (default: 1 minute). |
| GRIST_DATA_DIR | Directory in which to store documents. Defaults to `docs/` relative to the Grist application directory. In Grist's default Docker image, its default value is /persist/docs so that it will be used as a mounted volume. |
| GRIST_DEFAULT_EMAIL | if set, login as this user if no other credentials presented |
| GRIST_DEFAULT_PRODUCT | if set, this controls enabled features and limits of new sites. See names of PRODUCTS in Product.ts. |
| GRIST_DEFAULT_LOCALE | Locale to use as fallback when Grist cannot honour the browser locale. |
| GRIST_DOMAIN | in hosted Grist, Grist is served from subdomains of this domain.  Defaults to "getgrist.com". |
| GRIST_EXPERIMENTAL_PLUGINS | enables experimental plugins |
| GRIST_EXTERNAL_ATTACHMENTS_MODE | required to enable external storage for attachments. Set to "snapshots" to enable external storage. Default value is "none". Note that when enabled, a [snapshot storage has to be configured](https://support.getgrist.com/self-managed/#how-do-i-set-up-snapshots) as well. |
| GRIST_ENABLE_SERVICE_ACCOUNTS | enables the `service accounts` feature. This feature allows users to create special service accounts that they can manage and to whom they can grant restricted access to chosen resources. Useful as a way to get fine-grained api keys for use with third party automations. Unset by default |
| GRIST_ENABLE_REQUEST_FUNCTION | enables the REQUEST function. This function performs HTTP requests in a similar way to `requests.request`. This function presents a significant security risk, since it can let users call internal endpoints when Grist is available publicly. This function can also cause performance issues. Unset by default. |
| GRIST_HEADERS_TIMEOUT_MS | if set, override nodes's server.headersTimeout flag. |
| GRIST_HIDE_UI_ELEMENTS | comma-separated list of UI features to disable. Allowed names of parts: `helpCenter`, `billing`, `templates`, `createSite`, `multiSite`, `multiAccounts`, `importFromAirtable`, `sendToDrive`, `tutorials`, `supportGrist`, `themes`. If a part also exists in GRIST_UI_FEATURES, it will still be disabled. |
| GRIST_HOST | hostname to use when listening on a port. |
| GRIST_PROXY_FOR_UNTRUSTED_URLS | Full URL of proxy for delivering webhook payloads. Default value is `direct` for delivering payloads without proxying. |
| HTTPS_PROXY or https_proxy | Full URL of reverse web proxy (corporate proxy) for fetching the custom widgets repository or the OIDC config from the issuer. |
| GRIST_ID_PREFIX | for subdomains of form o-*, expect or produce o-${GRIST_ID_PREFIX}*. |
| GRIST_IGNORE_SESSION | if set, Grist will not use a session for authentication. |
| GRIST_INCLUDE_CUSTOM_SCRIPT_URL | if set, will load the referenced URL in a `<script>` tag on all app pages. |
| GRIST_INST_DIR | path to Grist instance configuration files, for Grist server. |
| GRIST_KEEP_ALIVE_TIMEOUT_MS | if set, override nodes's server.keepAliveTimeout flag. |
| GRIST_LIST_PUBLIC_SITES | if set to true, sites shared with the public will be listed for anonymous users. Defaults to false. |
| GRIST_MANAGED_WORKERS | if set, Grist can assume that if a url targeted at a doc worker returns a 404, that worker is gone |
| GRIST_MAX_NEW_USER_INVITES_PER_ORG | if set, limits the number of invites to new users per org. Once exceeded, additional invites are blocked until invited users log in for the first time or are uninvited
| GRIST_MAX_BILLING_MANAGERS_PER_ORG | if set, limits the number of billing managers per org |
| GRIST_MAX_PARALLEL_REQUESTS_PER_DOC| max number of concurrent API requests allowed per document (default is 10, set to 0 for unlimited) |
| GRIST_MAX_UPLOAD_ATTACHMENT_MB | max allowed size for attachments (0 or empty for unlimited). |
| GRIST_MAX_UPLOAD_IMPORT_MB | max allowed size for imports (except .grist files) (0 or empty for unlimited). |
| GRIST_OFFER_ALL_LANGUAGES | if set, all translated langauages are offered to the user (by default, only languages with a special 'good enough' key set are offered to user). |
| GRIST_ORG_IN_PATH | if true, encode org in path rather than domain |
| GRIST_PAGE_TITLE_SUFFIX | a string to append to the end of the `<title>` in HTML documents. Defaults to `" - Grist"`. Set to `_blank` for no suffix at all. |
| ~GRIST_PROXY_AUTH_HEADER~ | Deprecated, and interpreted as a synonym for GRIST_FORWARD_AUTH_HEADER. |
| GRIST_REQUEST_TIMEOUT_MS | if set, override nodes's server.requestTimeout flag. |
| GRIST_ROUTER_URL | optional url for an api that allows servers to be (un)registered with a load balancer |
| GRIST_SERVE_SAME_ORIGIN | set to "true" to access home server and doc workers on the same protocol-host-port as the top-level page, same as for custom domains (careful, host header should be trustworthy) |
| GRIST_SERVERS | the types of server to setup. Comma separated values which may contain "home", "docs", static" and/or "app". Defaults to "home,docs,static". |
| GRIST_SESSION_COOKIE | if set, overrides the name of Grist's cookie |
| GRIST_SESSION_DOMAIN | if set, associates the cookie with the given domain - otherwise defaults to GRIST_DOMAIN |
| GRIST_SESSION_SECRET | a key used to encode sessions |
| GRIST_SKIP_BUNDLED_WIDGETS | if set, Grist will ignore any bundled widgets included via NPM packages. |
| GRIST_SQLITE_MODE | if set to `wal`, use SQLite in [WAL mode](https://www.sqlite.org/wal.html), if set to `sync`, use SQLite with [SYNCHRONOUS=full](https://www.sqlite.org/pragma.html#pragma_synchronous)
| GRIST_ANON_PLAYGROUND | When set to `false` deny anonymous users access to the home page (but documents can still be shared to anonymous users). Defaults to `true`, unless GRIST_ORG_CREATION_ANYONE is `false`. |
| GRIST_FORCE_LOGIN | Setting it to `true` is similar to setting `GRIST_ANON_PLAYGROUND: false` but it blocks any anonymous access (thus any document shared publicly actually requires the users to be authenticated before consulting them) |
| GRIST_PERSONAL_ORGS | When set to `false` prevent new personal orgs from being created when a user signs up. Defaults to `true`, unless GRIST_ORG_CREATION_ANYONE is `false`. |
| GRIST_ORG_CREATION_ANYONE | When set to `false`, prevent new team orgs from being created by non-admin users. Sets default values of `GRIST_ANON_PLAYGROUND` and `GRIST_PERSONAL_ORGS` to `false`. Defaults to `true`. |
| GRIST_SINGLE_ORG | set to an org "domain" to pin client to that org |
| GRIST_TEMPLATE_ORG | set to an org "domain" to show public docs from that org |
| GRIST_HELP_CENTER | set the help center link ref |
| GRIST_TERMS_OF_SERVICE_URL | if set, adds terms of service link |
| FREE_COACHING_CALL_URL | set the link to the human help (example: email adress or meeting scheduling tool) |
| GRIST_CONTACT_SUPPORT_URL | set the link to contact support on error pages (example: email adress or online form) |
| GRIST_ONBOARDING_VIDEO_ID | set the ID of the YouTube video shown on the homepage and during onboarding |
| GRIST_CUSTOM_COMMON_URLS | overwrite the default commons URLs. Its value is expected to be a JSON object and a subset of the [ICommonUrls interface](./app/common/ICommonUrls.ts). |
| GRIST_SUPPORT_ANON | if set to 'true', show UI for anonymous access (not shown by default) |
| GRIST_SUPPORT_EMAIL | if set, give a user with the specified email support powers. The main extra power is the ability to share sites, workspaces, and docs with all users in a listed way. |
| GRIST_OPEN_GRAPH_PREVIEW_IMAGE | the URL of the preview image when sharing the link on websites like social medias or chat applications. |
| GRIST_TELEMETRY_LEVEL | the telemetry level. Can be set to: `off` (default), `limited`, or `full`. |
| GRIST_THROTTLE_CPU | if set, CPU throttling is enabled |
| GRIST_TRUST_PLUGINS | if set, plugins are expect to be served from the same host as the rest of the Grist app, rather than from a distinct host. Ordinarily, plugins are served from a distinct host so that the cookies used by the Grist app are not automatically available to them. Enable this only if you understand the security implications. |
| GRIST_USER_ROOT | an extra path to look for plugins in - Grist will scan for plugins in `$GRIST_USER_ROOT/plugins`. |
| GRIST_UI_FEATURES | comma-separated list of UI features to enable. Allowed names of parts: `helpCenter`, `billing`, `templates`, `createSite`, `multiSite`, `multiAccounts`, `importFromAirtable`, `sendToDrive`, `tutorials`, `supportGrist`, `themes`. If a part also exists in GRIST_HIDE_UI_ELEMENTS, it won't be enabled. |
| GRIST_UNTRUSTED_PORT | if set, plugins will be served from the given port. This is an alternative to setting APP_UNTRUSTED_URL. |
| GRIST_WIDGET_LIST_URL | a url pointing to a widget manifest, by default https://github.com/gristlabs/grist-widget/releases/download/latest/manifest.json is used |
| GRIST_LOG_HTTP | When set to `true`, log HTTP requests and responses information. Defaults to `false`. |
| GRIST_LOG_HTTP_BODY | When this variable and `GRIST_LOG_HTTP` are set to `true` , log the body along with the HTTP requests. :warning: Be aware it may leak confidential information in the logs.:warning: Defaults to `false`. |
| GRIST_LOG_AS_JSON | When this variable is set to `true` or a truthy value, output log lines in JSON as opposed to a plain text format. |
| GRIST_LOG_API_DETAILS | When this variable is set to `true` or a truthy value, log the API calls details. |
| COOKIE_MAX_AGE | session cookie max age, defaults to 90 days; can be set to "none" to make it a session cookie |
| HOME_PORT | port number to listen on for REST API server; if set to "share", add API endpoints to regular grist port. |
| PORT | port number to listen on for Grist server |
| REDIS_URL | optional redis server for browser sessions and db query caching |
| GRIST_SNAPSHOT_TIME_CAP | optional. Define the caps for tracking buckets. Usage: {"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000} |
| GRIST_SNAPSHOT_KEEP | optional. Number of recent snapshots to retain unconditionally for a document, regardless of when they were made |
| GRIST_PROMCLIENT_PORT | optional. If set, serve the Prometheus metrics on the specified port number. ⚠️ Be sure to use a port which is not publicly exposed ⚠️. |
| GRIST_ENABLE_SCIM | optional. If set, enable the [SCIM API Endpoint](https://support.getgrist.com/install/scim/) (experimental) |
| GRIST_LOGIN_SYSTEM_TYPE | optional. If set, explicitly selects which login system to use. Valid values: `saml`, `oidc`, `forward-auth`, `minimal`. If not set, Grist will automatically detect and use the first configured login system. |
| GRIST_OIDC_... | optional. Environment variables used to configure OpenID authentification. See [OpenID Connect](https://support.getgrist.com/install/oidc/) documentation for full related list of environment variables. |
| GRIST_SAML_... | optional. Environment variables used to configure SAML authentification. See [SAML](https://support.getgrist.com/install/saml/) documentation for full related list of environment variables. |
| GRIST_IDP_EXTRA_PROPS | optional. If set, defines which extra fields returned by your identity provider will be stored in the users table of the home database (in the `options.ssoExtraInfo` object). Usage: 'onekey,anotherkey'. |
| GRIST_FEATURE_FORM_FRAMING | optional. Configures a border around a rendered form that is added for security reasons; Can be set to: `border` or `minimal`. Defaults to `border`. |
| GRIST_TRUTHY_VALUES | optional. Comma-separated list of extra words that should be considered as truthy by the data engine beyond english defaults. Ex: "oui,ja,si" |
| GRIST_FALSY_VALUES | optional. Comma-separated list of extra words that should be considered as falsy by the data engine beyond english defaults. Ex: "non,nein,no" |
| GRIST_ENABLE_USER_PRESENCE | optional, enabled by default. If set to 'false', disables all user presence features. |
#### AI Formula Assistant related variables (all optional):

Variable | Purpose
-------- | -------
ASSISTANT_API_KEY   | optional. An API key to pass when making requests to an external AI conversational endpoint.
ASSISTANT_CHAT_COMPLETION_ENDPOINT  | optional. A chat-completion style endpoint to call. Not needed if OpenAI is being used.
ASSISTANT_MODEL     | optional. If set, this string is passed along in calls to the AI conversational endpoint.
ASSISTANT_LONGER_CONTEXT_MODEL     | optional. If set, requests that fail because of a context length limitation will be retried with this model set.
OPENAI_API_KEY      | optional. Synonym for ASSISTANT_API_KEY that assumes an OpenAI endpoint is being used. Sign up for an account on OpenAI and then generate a secret key [here](https://platform.openai.com/account/api-keys).

At the time of writing, the AI Assistant is known to function against OpenAI chat completion endpoints (those ending in `/v1/chat/completions`).
It is also known to function against the chat completion endpoint provided by <a href="https://github.com/abetlen/llama-cpp-python">llama-cpp-python</a> and by [LM Studio](https://lmstudio.ai/). For useful results, the LLM should be on par with GPT 3.5 or above.

#### Sandbox related variables:

Variable | Purpose | Sandbox |
-------- | ------- | ------- |
GRIST_SANDBOX_FLAVOR | can be gvisor, pynbox, unsandboxed, docker, or macSandboxExec. If set, forces Grist to use the specified kind of sandbox. | N/A |
GRIST_SANDBOX | a program or image name to run as the sandbox. See NSandbox.ts for nerdy details. | N/A |
GVISOR_LIMIT_NPROC | the number of extant processes the sandbox is allowed to spawn when running on Linux. Defaults to 8. | GVisor |
GVISOR_LIMIT_MEMORY | the maximum size of the sandboxed process's virtual memory (in bytes). No limit by default. | GVisor |

#### Forward authentication variables:

Variable | Purpose
-------- | -------
GRIST_FORWARD_AUTH_HEADER | if set, trust the specified header (e.g. "x-forwarded-user") to contain authorized user emails, and enable "forward auth" logins.
GRIST_FORWARD_AUTH_LOGIN_PATH | if GRIST_FORWARD_AUTH_HEADER is set, Grist will listen at this path for logins. Defaults to `/auth/login`.
GRIST_FORWARD_AUTH_LOGOUT_PATH | if GRIST_FORWARD_AUTH_HEADER is set, Grist will forward to this path when user logs out.

Forward authentication supports two modes, distinguished by `GRIST_IGNORE_SESSION`:

1. With sessions, and forward-auth on login endpoints.

   For example, using traefik reverse proxy with
   [traefik-forward-auth](https://github.com/thomseddon/traefik-forward-auth) middleware:

   - `GRIST_IGNORE_SESSION`: do NOT set, or set to a falsy value.
   - Make sure your reverse proxy applies the forward auth middleware to
     `GRIST_FORWARD_AUTH_LOGIN_PATH` and `GRIST_FORWARD_AUTH_LOGOUT_PATH`.
   - If you want to allow anonymous access in some cases, make sure all other paths are free of
     the forward auth middleware. Grist will trigger it as needed by redirecting to
     `GRIST_FORWARD_AUTH_LOGIN_PATH`. Once the user is logged in, Grist will use sessions to
     identify the user until logout.

2. With no sessions, and forward-auth on all endpoints.

   For example, using HTTP Basic Auth and server configuration that sets the header (specified in
   `GRIST_FORWARD_AUTH_HEADER`) to the logged-in user.

  - `GRIST_IGNORE_SESSION`: set to `true`. Grist sessions will not be used.
  - Make sure your reverse proxy sets the header you specified for all requests that may need
    login information. It is imperative that this header cannot be spoofed by the user, since
    Grist will trust whatever is in it.

When using forward authentication, you may wish to also set the following variables:

  * `GRIST_FORCE_LOGIN=true` to disable anonymous access.

#### Plugins:

Grist has a plugin system, used internally. One useful thing you can
do with it is include custom widgets in a build of Grist. Custom widgets
are usually made available just by setting `GRIST_WIDGET_LIST_URL`,
but that has the downside of being an external dependency, which can
be awkward for offline use or for archiving. Plugins offer an alternative.

To "bundle" custom widgets as a plugin:

 * Add a subdirectory of `plugins`, e.g. `plugins/my-widgets`.
   Alternatively, you can set the `GRIST_USER_ROOT` environment
   variable to any path you want, and then create `plugins/my-widgets`
   within that.
 * Add a `manifest.yml` file in that subdirectory that looks like
   this:

```
name: My Widgets
components:
  widgets: widgets.json
```

 * The `widgets.json` file should be in the format produced by
   the [grist-widget](https://github.com/gristlabs/grist-widget)
   repository, and should be placed in the same directory as
   `manifest.yml`. Any material in `plugins/my-widgets`
   will be served by Grist, and relative URLs can be used in
   `widgets.json`.
 * Once all files are in place, restart Grist. Your widgets should
   now be available in the custom widgets dropdown, along with
   any others from `GRIST_WIDGET_LIST_URL`.
 * If you like, you can add multiple plugin subdirectories, with
   multiple sets of widgets, and they'll all be made available.

#### Google Drive integrations:

Variable | Purpose
-------- | -------
GOOGLE_CLIENT_ID    | set to the Google Client Id to be used with Google API client
GOOGLE_CLIENT_SECRET| set to the Google Client Secret to be used with Google API client
GOOGLE_API_KEY      | set to the Google API Key to be used with Google API client (accessing public files)
GOOGLE_DRIVE_SCOPE  | set to the scope requested for Google Drive integration (defaults to drive.file)

#### Database variables:

Variable | Purpose
-------- | -------
TYPEORM_DATABASE | database filename for sqlite or database name for other db types
TYPEORM_HOST     | host for db
TYPEORM_LOGGING  | set to 'true' to see all sql queries
TYPEORM_PASSWORD | password to use
TYPEORM_PORT     | port number for db if not the default for that db type
TYPEORM_TYPE     | set to 'sqlite' or 'postgres'
TYPEORM_USERNAME | username to connect as
TYPEORM_EXTRA    | any other properties to pass to TypeORM in JSON format

#### Docker-only variables:

Variable | Purpose
---------|--------
GRIST_DOCKER_USER  | optional. When the container runs as the root user, this is the user the Grist services run as. Overrides the default.
GRIST_DOCKER_GROUP | optional. When the container runs as the root user, this is the group the Grist services run as. Overrides the default.

#### Testing:

Variable | Purpose
-------- | -------
GRIST_TESTING_SOCKET    | a socket used for out-of-channel communication during tests only.
GRIST_TEST_FORCE_LIGHT_MODE | if set, Grist will use light mode even if system preference is dark. Some tests just assume light mode.
GRIST_TEST_HTTPS_OFFSET | if set, adds https ports at the specified offset.  This is useful in testing.
GRIST_TEST_SSL_CERT     | if set, contains filename of SSL certificate.
GRIST_TEST_SSL_KEY      | if set, contains filename of SSL private key.
GRIST_TEST_LOGIN        | allow fake unauthenticated test logins (suitable for dev environment only).
GRIST_TEST_ROUTER       | if set, then the home server will serve a mock version of router api at /test/router
GREP_TESTS              | pattern for selecting specific tests to run (e.g. `env GREP_TESTS=ActionLog yarn test`).

## Tests

Tests are run automatically as part of CI when a PR is opened. However, it can be helpful to run tests locally
before pushing your changes to GitHub. First, you'll want to make sure you've installed all dependencies:

```
yarn install
yarn install:python
```

Then, you can run the main test suite like so:

```
yarn test
```

Python tests may also be run locally. (Note: currently requires Python 3.10 - 3.11.)

```
yarn test:python
```

For running specific tests, you can specify a pattern with the `GREP_TESTS` variable:

```
env GREP_TESTS=ChoiceList yarn test
env GREP_TESTS=summary yarn test:python
```

## License

This repository, `grist-core`, is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0), which is an [OSI](https://opensource.org/)-approved free software license. See LICENSE.txt and NOTICE.txt for more information.


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

| Version  | Supported          |
| -------  | ------------------ |
| >= 1.3.2 | :white_check_mark: |
| < 1.3.2  | upgrade required   |

## Reporting a Vulnerability

1. **Contact us** by sending an email to **[security@getgrist.com](mailto:security@getgrist.com)** with the following information:
   - A description of the vulnerability.
   - Steps to reproduce the issue.
   - Any known impacts or suggested fixes.

2. **Our response:**  
   - We will acknowledge your report within **three working days**.
   - We will collaborate with you to verify and address the issue.
   - Once resolved, we’ll release a patch and notify users.


================================================
FILE: app/cli.sh
================================================
#!/usr/bin/env bash

NODE_PATH=_build:_build/stubs:_build/ext node _build/app/server/companion.js "$@"


================================================
FILE: app/client/DefaultHooks.ts
================================================
import { UrlTweaks } from "app/common/gristUrls";

import { IAttrObj } from "grainjs";

export interface IHooks {
  iframeAttributes?: Record<string, any>,
  fetch?: typeof fetch,
  baseURI?: string,
  urlTweaks?: UrlTweaks,

  /**
   * Modify the attributes of an <a> dom element.
   * Convenient in grist-static to directly hook up a
   * download link with the function that provides the data.
   */
  maybeModifyLinkAttrs(attrs: IAttrObj): IAttrObj;
}

export const defaultHooks: IHooks = {
  maybeModifyLinkAttrs(attrs: IAttrObj) {
    return attrs;
  },
};


================================================
FILE: app/client/Hooks.ts
================================================
import { defaultHooks } from "app/client/DefaultHooks";

export const hooks = defaultHooks;


================================================
FILE: app/client/aclui/ACLColumnList.ts
================================================
/**
 * Implements a widget for showing and editing a list of colIds. It offers a select dropdown to
 * add a new column, and allows removing already-added columns.
 */
import { aclSelect, cssSelect } from "app/client/aclui/ACLSelect";
import { testId, theme } from "app/client/ui2018/cssVars";
import { icon } from "app/client/ui2018/icons";

import { Computed, dom, Observable, styled } from "grainjs";

export function aclColumnList(colIds: Observable<string[]>, validColIds: string[]) {
  // Define some helpers functions.
  function removeColId(colId: string) {
    colIds.set(colIds.get().filter(c => (c !== colId)));
  }
  function addColId(colId: string) {
    colIds.set([...colIds.get(), colId]);
    selectBox.focus();
  }
  function onFocus(ev: FocusEvent) {
    editing.set(true);
    // Focus the select box, except when focus just moved from it, e.g. after Shift-Tab.
    if (ev.relatedTarget !== selectBox) {
      selectBox.focus();
    }
  }
  function onBlur() {
    if (!selectBox.matches(".weasel-popup-open") && colIds.get().length > 0) {
      editing.set(false);
    }
  }

  // The observable for the selected element is a Computed, with a callback for being set, which
  // adds the selected colId to the list.
  const newColId = Computed.create(null, use => "")
    .onWrite((value) => { setTimeout(() => addColId(value), 0); });

  // We don't allow adding the same column twice, so for the select dropdown build a list of
  // unused colIds.
  const unusedColIds = Computed.create(null, colIds, (use, _colIds) => {
    const used = new Set(_colIds);
    return validColIds.filter(c => !used.has(c));
  });

  // The "editing" observable determines which of two states is active: to show or to edit.
  const editing = Observable.create(null, !colIds.get().length);

  let selectBox: HTMLElement;
  return cssColListWidget({ tabIndex: "0" },
    dom.autoDispose(unusedColIds),
    cssColListWidget.cls("-editing", editing),
    dom.on("focus", onFocus),
    dom.forEach(colIds, colId =>
      cssColItem(
        cssColId(colId),
        cssColItemIcon(icon("CrossSmall"),
          dom.on("click", () => removeColId(colId)),
          testId("acl-col-remove"),
        ),
        testId("acl-column"),
      ),
    ),
    cssNewColItem(
      dom.update(
        selectBox = aclSelect(newColId, unusedColIds, { defaultLabel: "[Add Column]" }),
        cssSelect.cls("-active"),
        dom.on("blur", onBlur),
        dom.onKeyDown({ Escape: onBlur }),
        // If starting out in edit mode, focus the select box.
        (editing.get() ? (elem) => { setTimeout(() => elem.focus(), 0); } : null),
      ),
    ),
  );
}

const cssColListWidget = styled("div", `
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: relative;
  outline: none;
  margin: 6px 8px;
  cursor: pointer;
  border-radius: 4px;

  border: 1px solid transparent;
  &:not(&-editing):hover {
    border: 1px solid ${theme.accessRulesColumnListBorder};
  }
`);

const cssColItem = styled("div", `
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-radius: 3px;
  padding-left: 6px;
  padding-right: 2px;
  color: ${theme.accessRulesColumnItemFg};

  .${cssColListWidget.className}-editing & {
    background-color: ${theme.accessRulesColumnItemBg};
  }
`);

const cssColId = styled("div", `
  flex: auto;
  height: 24px;
  line-height: 24px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
`);

const cssNewColItem = styled("div", `
  margin-top: 2px;
  display: none;
  .${cssColListWidget.className}-editing & {
    display: flex;
  }
`);

const cssColItemIcon = styled("div", `
  flex: none;
  height: 16px;
  width: 16px;
  border-radius: 16px;
  display: none;
  cursor: default;
  --icon-color: ${theme.accessRulesColumnItemIconFg};
  &:hover {
    background-color: ${theme.accessRulesColumnItemIconHoverBg};
    --icon-color: ${theme.accessRulesColumnItemIconHoverFg};
  }
  .${cssColListWidget.className}-editing & {
    display: flex;
  }
`);


================================================
FILE: app/client/aclui/ACLFormulaEditor.ts
================================================
import { setupAceEditorCompletions } from "app/client/components/AceEditorCompletions";
import { expandAndFilterSuggestions, ISuggestionWithSubAttrs } from "app/client/lib/Suggestions";
import { theme } from "app/client/ui2018/cssVars";
import { gristThemeObs } from "app/client/ui2018/theme";
import { ISuggestionWithValue } from "app/common/ActiveDocAPI";
import { Theme } from "app/common/ThemePrefs";

import ace, { Ace } from "ace-builds";
import { dom, DomArg, Observable, styled } from "grainjs";
import debounce from "lodash/debounce";

export interface ACLFormulaOptions {
  initialValue: string;
  readOnly: boolean;
  placeholder: DomArg;
  setValue: (value: string) => void;
  getSuggestions: () => ISuggestionWithSubAttrs[];
  customiseEditor?: (editor: Ace.Editor) => void;
}

export function aclFormulaEditor(options: ACLFormulaOptions) {
  // Create an element and an editor within it.
  const editorElem = dom("div");
  const editor: Ace.Editor = ace.edit(editorElem);

  // Set various editor options.
  function setAceTheme(newTheme: Theme) {
    const { appearance } = newTheme;
    const aceTheme = appearance === "dark" ? "dracula" : "chrome";
    editor.setTheme(`ace/theme/${aceTheme}`);
  }
  setAceTheme(gristThemeObs().get());
  const themeListener = gristThemeObs().addListener((newTheme) => {
    setAceTheme(newTheme);
  });
  // ACE editor resizes automatically when maxLines is set.
  editor.setOptions({ enableLiveAutocompletion: true, maxLines: 10 });
  editor.renderer.setShowGutter(false);       // Default line numbers to hidden
  editor.renderer.setPadding(5);
  editor.renderer.setScrollMargin(4, 4, 0, 0);
  (editor as any).$blockScrolling = Infinity;
  editor.setReadOnly(options.readOnly);
  editor.setFontSize("12");
  editor.setHighlightActiveLine(false);

  const session = editor.getSession();
  session.setMode("ace/mode/python");
  session.setTabSize(2);
  session.setUseWrapMode(true);

  // Implement placeholder text since the version of ACE we use doesn't support one.
  const showPlaceholder = Observable.create(null, !options.initialValue.length);
  editor.renderer.scroller.appendChild(
    cssAcePlaceholder(dom.show(showPlaceholder), options.placeholder),
  );
  editor.on("change", () => showPlaceholder.set(!editor.getValue().length));

  async function getSuggestions(prefix: string): Promise<ISuggestionWithValue[]> {
    return [
      // The few Python keywords and constants we support.
      "and", "or", "not", "in", "is", "True", "False", "None",
      // Some grist-specific constants:
      "OWNER", "EDITOR", "VIEWER",
      // The common variables.
      "user", "rec", "newRec",
    ]
      .map<ISuggestionWithValue>(suggestion => [suggestion, null])   // null means no example value
      .concat(
      // Other completions that depend on doc schema or other rules.
        expandAndFilterSuggestions(prefix, options.getSuggestions())
          .map<ISuggestionWithValue>(s => [s.value, s.example || null]),
      );
  }

  setupAceEditorCompletions(editor, { getSuggestions });

  // Save on blur.
  editor.on("blur", () => options.setValue(editor.getValue()));

  // Save changes every 1 second
  const save = debounce(() => options.setValue(editor.getValue()), 1000);
  editor.on("change", save);

  // Blur (and save) on Enter key.
  editor.commands.addCommand({
    name: "onEnter",
    bindKey: { win: "Enter", mac: "Enter" },
    exec: () => editor.blur(),
  });
  // Disable Tab/Shift+Tab commands to restore their regular behavior.
  (editor.commands as any).removeCommands(["indent", "outdent"]);

  // Set the editor's initial value.
  editor.setValue(options.initialValue);

  if (options.customiseEditor) {
    options.customiseEditor(editor);
  }

  return cssConditionInputAce(
    dom.autoDispose(themeListener ?? null),
    cssConditionInputAce.cls("-disabled", options.readOnly),
    // ACE editor calls preventDefault on clicks into the scrollbar area, which prevents focus
    // being set when the click happens to be into there. To ensure we can focus on such clicks
    // anyway, listen to the mousedown event in the capture phase.
    dom.on("mousedown", () => { editor.focus(); }, { useCapture: true }),
    dom.onDispose(() => editor.destroy()),
    dom.onDispose(() => save.cancel()),
    editorElem,
  );
}

const cssConditionInputAce = styled("div", `
  width: 100%;
  min-height: 28px;
  padding: 1px;
  border-radius: 3px;
  border: 1px solid transparent;
  cursor: pointer;

  &:hover {
    border: 1px solid ${theme.accessRulesFormulaEditorBorderHover};
  }
  &:not(&-disabled):focus-within {
    box-shadow: inset 0 0 0 1px ${theme.accessRulesFormulaEditorFocus};
    border-color: ${theme.accessRulesFormulaEditorFocus};
  }
  &:not(:focus-within) .ace_scroller, &-disabled .ace_scroller {
    cursor: unset;
  }
  &-disabled, &-disabled:hover {
    background-color: ${theme.accessRulesFormulaEditorBgDisabled};
    box-shadow: unset;
    border-color: transparent;
  }
  & .ace-chrome, & .ace-dracula {
    background-color: ${theme.accessRulesFormulaEditorBg};
  }
  &:not(:focus-within) .ace_print-margin {
    width: 0px;
  }
  &-disabled .ace-chrome, &-disabled .ace-dracula {
    background-color: ${theme.accessRulesFormulaEditorBgDisabled};
  }
  & .ace_marker-layer, & .ace_cursor-layer {
    display: none;
  }
  &:not(&-disabled) .ace_focus .ace_marker-layer, &:not(&-disabled) .ace_focus .ace_cursor-layer {
    display: block;
  }
`);

const cssAcePlaceholder = styled("div", `
  padding: 4px 5px;
  opacity: 0.5;
`);


================================================
FILE: app/client/aclui/ACLMemoEditor.ts
================================================
import { theme } from "app/client/ui2018/cssVars";

import { dom, DomElementArg, Observable, styled } from "grainjs";

export function aclMemoEditor(obs: Observable<string>, ...args: DomElementArg[]): HTMLInputElement {
  return cssMemoInput(
    dom.prop("value", obs),
    dom.on("input", (_e, elem) => obs.set(elem.value)),
    ...args,
  );
}

const cssMemoInput = styled("input", `
  width: 100%;
  min-height: 28px;
  padding: 4px 5px;
  border-radius: 3px;
  border: 1px solid transparent;
  cursor: pointer;
  color: ${theme.controlFg};
  background-color: ${theme.inputBg};
  caret-color : ${theme.inputFg};
  font: 12px 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
  overflow: hidden;
  text-overflow: ellipsis;

  &:hover {
    border: 1px solid ${theme.inputBorder};
  }
  &:not(&-disabled):focus-within {
    outline: none !important;
    cursor: text;
    box-shadow: inset 0 0 0 1px ${theme.controlFg};
    border-color: ${theme.controlFg};
  }
`);


================================================
FILE: app/client/aclui/ACLSelect.ts
================================================
import { theme } from "app/client/ui2018/cssVars";
import { icon } from "app/client/ui2018/icons";
import { IOption, select } from "app/client/ui2018/menus";

import { MaybeObsArray, Observable, styled } from "grainjs";
import * as weasel from "popweasel";

/**
 * A styled version of select() from ui2018/menus, for use in the AccessRules page.
 */
export function aclSelect<T>(obs: Observable<T>, optionArray: MaybeObsArray<IOption<T>>,
  options: weasel.ISelectUserOptions = {}) {
  return cssSelect(obs, optionArray, { buttonArrow: cssSelectArrow("Collapse"), ...options });
}

export const cssSelect = styled(select, `
  height: 28px;
  width: 100%;
  border: 1px solid transparent;
  cursor: pointer;

  &:hover, &:focus, &.weasel-popup-open, &-active {
    border: 1px solid ${theme.selectButtonBorder};
    box-shadow: none;
  }
`);

const cssSelectCls = cssSelect.className;

const cssSelectArrow = styled(icon, `
  margin: 0 2px;
  pointer-events: none;
  display: none;

  .${cssSelectCls}:hover &, .${cssSelectCls}:focus &, .weasel-popup-open &, .${cssSelectCls}-active & {
    display: flex;
  }
`);


================================================
FILE: app/client/aclui/ACLUsers.ts
================================================
import { makeT } from "app/client/lib/localization";
import { DocPageModel } from "app/client/models/DocPageModel";
import { urlState } from "app/client/models/gristUrlState";
import { createUserImage } from "app/client/ui/UserImage";
import { cssMemberImage, cssMemberListItem, cssMemberPrimary,
  cssMemberSecondary, cssMemberText } from "app/client/ui/UserItem";
import { testId, theme, vars } from "app/client/ui2018/cssVars";
import { gristFloatingMenuClass, menu, menuCssClass, menuItemLink } from "app/client/ui2018/menus";
import { PermissionDataWithExtraUsers } from "app/common/ActiveDocAPI";
import { IGristUrlState, userOverrideParams } from "app/common/gristUrls";
import { waitGrainObs } from "app/common/gutil";
import { FullUser } from "app/common/LoginSessionAPI";
import { ANONYMOUS_USER_EMAIL, EVERYONE_EMAIL } from "app/common/UserAPI";
import { getRealAccess, UserAccessData } from "app/common/UserAPI";
import { getUserRoleText } from "app/common/UserAPI";

import { Disposable, dom, Observable, styled } from "grainjs";
import noop from "lodash/noop";
import { cssMenu, cssMenuWrap, defaultMenuOptions, IMenuOptions, IPopupOptions, setPopupToCreateDom } from "popweasel";

const t = makeT("ViewAsDropdown");
const userT = makeT("UserManagerModel");

function isSpecialEmail(email: string) {
  return email === ANONYMOUS_USER_EMAIL || email === EVERYONE_EMAIL;
}

export class ACLUsersPopup extends Disposable {
  public readonly isInitialized = Observable.create(this, false);
  public readonly allUsers = Observable.create<UserAccessData[]>(this, []);
  private _shareUsers: UserAccessData[] = [];           // Users doc is shared with.
  private _attributeTableUsers: UserAccessData[] = [];  // Users mentioned in attribute tables.
  private _exampleUsers: UserAccessData[] = [];         // Example users.
  private _currentUser: FullUser | null = null;

  constructor(public pageModel: DocPageModel,
    private _fetch: () => Promise<PermissionDataWithExtraUsers | null> = () => this._fetchData()) {
    super();
  }

  public async load() {
    const permissionData = await this._fetch();
    if (this.isDisposed()) { return; }
    this.init(permissionData);
  }

  public getUsers() {
    const users = [...this._shareUsers, ...this._attributeTableUsers];
    if (this._showExampleUsers()) { users.push(...this._exampleUsers); }
    return users;
  }

  public init(permissionData: PermissionDataWithExtraUsers | null) {
    const pageModel = this.pageModel;
    this._currentUser = pageModel.userOverride.get()?.user || pageModel.appModel.currentValidUser;

    if (permissionData) {
      this._shareUsers = permissionData.users.map(user => ({
        ...user,
        access: getRealAccess(user, permissionData),
      }))
        .filter(user => user.access && !isSpecialEmail(user.email))
        .filter(user => this._currentUser?.id !== user.id);
      this._attributeTableUsers = permissionData.attributeTableUsers;
      this._exampleUsers = permissionData.exampleUsers;
      this.allUsers.set(this.getUsers());
      this.isInitialized.set(true);
    }
  }

  // Optionally have document page reverts to the default page upon activation of the view as mode
  // by setting `options.resetDocPage` to true.
  public attachPopup(elem: Element, options: IPopupOptions & { resetDocPage?: boolean }) {
    setPopupToCreateDom(elem, (ctl) => {
      const buildRow =
        (user: UserAccessData) => this._buildUserRow(user, options);
      const buildExampleUserRow =
        (user: UserAccessData) => this._buildUserRow(user, { isExampleUser: true, ...options });
      return cssMenuWrap(cssMenu(
        dom.cls(menuCssClass),
        dom.cls(gristFloatingMenuClass),
        cssUsers.cls(""),
        cssHeader(t("Shared users"), dom.show(this._shareUsers.length > 0)),
        dom.forEach(this._shareUsers, buildRow),
        (this._attributeTableUsers.length > 0) ? cssHeader(t("Other users from table")) : null,
        dom.forEach(this._attributeTableUsers, buildExampleUserRow),
        // Include example users only if there are not many "real" users.
        // It might be better to have an expandable section with these users, collapsed
        // by default, but that's beyond my UI ken.
        this._showExampleUsers() ? [
          (this._exampleUsers.length > 0) ? cssHeader(t("Example Users")) : null,
          dom.forEach(this._exampleUsers, buildExampleUserRow),
        ] : null,
        (el) => { setTimeout(() => el.focus(), 0); },
        dom.onKeyDown({ Escape: () => ctl.close() }),
      ));
    }, { ...defaultMenuOptions, ...options });
  }

  // See 'attachPopup' for more info on the 'resetDocPage' option.
  public menu(options: IMenuOptions) {
    return menu(() => {
      this.load().catch(noop);
      return [
        cssMenuHeader("view as"),
        dom.forEach(this.allUsers, user => menuItemLink(
          `${user.name || user.email} (${getUserRoleText(user)})`,
          testId("acl-user-access"),
          this._viewAs(user),
        )),
      ];
    }, options);
  }

  private async _fetchData() {
    const doc = this.pageModel.currentDoc.get();
    const gristDoc = await waitGrainObs(this.pageModel.gristDoc);
    return doc && gristDoc.docComm.getUsersForViewAs();
  }

  private _showExampleUsers() {
    return this._shareUsers.length + this._attributeTableUsers.length < 5;
  }

  private _buildUserRow(user: UserAccessData, opt: { isExampleUser?: boolean, resetDocPage?: boolean } = {}) {
    return dom("a",
      { class: cssMemberListItem.className + " " + cssUserItem.className },
      cssMemberImage(
        createUserImage(opt.isExampleUser ? "exampleUser" : user, "large"),
      ),
      cssMemberText(
        cssMemberPrimary(user.name || dom("span", user.email),
          cssRole("(", userT(getUserRoleText(user)), ")", testId("acl-user-access")),
        ),
        user.name ? cssMemberSecondary(user.email) : null,
      ),
      this._viewAs(user, opt.resetDocPage),
      testId("acl-user-item"),
    );
  }

  private _viewAs(user: UserAccessData, resetDocPage: boolean = false) {
    const extraState: IGristUrlState = {};
    if (resetDocPage) { extraState.docPage = undefined; }
    if (this.pageModel?.isPrefork.get() &&
      this.pageModel?.currentDoc.get()?.access !== "owners") {
      // "View As" is restricted to document owners on the back-end. Non-owners can be
      // permitted to pretend to be owners of a pre-forked document, but if they want
      // to do "View As", that would be layering pretence over pretense. Better to just
      // go ahead and create the fork, so the user becomes a genuine owner, so the
      // back-end doesn't have to become too metaphysical (and maybe hard to review).
      return dom.on("click", async () => {
        const forkResult = await this.pageModel?.gristDoc.get()?.docComm.fork();
        if (!forkResult) { throw new Error("Failed to create fork"); }
        window.location.assign(urlState().makeUrl(userOverrideParams(user.email,
          { ...extraState, doc: forkResult.urlId })));
      });
    } else {
      // When forking isn't needed, we return a direct link to be maximally transparent
      // about where button will go.
      return urlState().setHref(userOverrideParams(user.email, extraState));
    }
  }
}

const cssUsers = styled("div", `
  max-width: unset;
`);

const cssUserItem = styled(cssMemberListItem, `
  width: auto;
  padding: 8px 16px;
  align-items: center;
  &:hover {
    background-color: ${theme.lightHover};
  }
  &, &:hover, &:focus {
    text-decoration: none;
  }
`);

const cssRole = styled("span", `
  margin: 0 8px;
  font-weight: normal;
`);

const cssHeader = styled("div", `
  margin: 11px 24px 14px 24px;
  font-weight: 700;
  text-transform: uppercase;
  font-size: ${vars.xsmallFontSize};
  color: ${theme.darkText};
`);

const cssMenuHeader = styled("div", `
  margin: 8px 24px;
  margin-bottom: 4px;
  font-weight: 700;
  text-transform: uppercase;
  font-size: ${vars.xsmallFontSize};
  color: ${theme.darkText};
`);


================================================
FILE: app/client/aclui/AccessRules.ts
================================================
/**
 * UI for managing granular ACLs.
 */
import { aclColumnList } from "app/client/aclui/ACLColumnList";
import { aclFormulaEditor } from "app/client/aclui/ACLFormulaEditor";
import { aclMemoEditor } from "app/client/aclui/ACLMemoEditor";
import { aclSelect } from "app/client/aclui/ACLSelect";
import { ACLUsersPopup } from "app/client/aclui/ACLUsers";
import { permissionsWidget } from "app/client/aclui/PermissionsWidget";
import { GristDoc } from "app/client/components/GristDoc";
import { makeT } from "app/client/lib/localization";
import { inlineMarkdown, markdown } from "app/client/lib/markdown";
import { ISuggestionWithSubAttrs } from "app/client/lib/Suggestions";
import { logTelemetryEvent } from "app/client/lib/telemetry";
import { reportError, UserError } from "app/client/models/errors";
import { TableData } from "app/client/models/TableData";
import { shadowScroll } from "app/client/ui/shadowScroll";
import { withInfoTooltip } from "app/client/ui/tooltips";
import { bigBasicButton, bigPrimaryButton } from "app/client/ui2018/buttons";
import { squareCheckbox } from "app/client/ui2018/checkbox";
import { mediaSmall, testId, theme } from "app/client/ui2018/cssVars";
import { textInput } from "app/client/ui2018/editableLabel";
import { cssIconButton, icon } from "app/client/ui2018/icons";
import { cssNestedLinks } from "app/client/ui2018/links";
import { loadingSpinner } from "app/client/ui2018/loaders";
import { menu, menuItemAsync } from "app/client/ui2018/menus";
import { confirmModal } from "app/client/ui2018/modals";
import {
  AVAILABLE_BITS_COLUMNS,
  AVAILABLE_BITS_TABLES,
  emptyPermissionSet,
  MixedPermissionValue,
  parsePermissions,
  PartialPermissionSet,
  PermissionKey,
  permissionSetToText,
  summarizePermissions,
  summarizePermissionSet,
  trimPermissions,
} from "app/common/ACLPermissions";
import { ACLRuleCollection, isSchemaEditResource, SPECIAL_RULES_TABLE_ID } from "app/common/ACLRuleCollection";
import { SpecialRuleName } from "app/common/ACLRuleCollection";
import { AclRuleProblem, AclTableDescription, getTableTitle } from "app/common/ActiveDocAPI";
import { BulkColValues, getColValues, RowRecord, UserAction } from "app/common/DocActions";
import {
  RulePart,
  RuleSet,
  UserAttributeRule,
} from "app/common/GranularAccessClause";
import { getDefaultForType, isHiddenCol } from "app/common/gristTypes";
import { commonUrls } from "app/common/gristUrls";
import { isNonNullish, localeCompare, unwrap } from "app/common/gutil";
import {
  getPredicateFormulaProperties,
  ParsedPredicateFormula,
  PredicateFormulaProperties,
  typeCheckFormula,
} from "app/common/PredicateFormula";
import { EmptyRecordView, InfoView, RecordView } from "app/common/RecordView";
import { SchemaTypes } from "app/common/schema";
import { MetaRowRecord } from "app/common/TableData";
import { tokens } from "app/common/ThemePrefs";

import {
  BaseObservable,
  Computed,
  Disposable,
  dom,
  DomContents,
  DomElementArg,
  IDisposableOwner,
  MutableObsArray,
  obsArray,
  Observable,
  styled,
} from "grainjs";
import isEqual from "lodash/isEqual";

const t = makeT("AccessRules");

// Types for the rows in the ACL tables we use.
type ResourceRec = SchemaTypes["_grist_ACLResources"] & { id?: number };
type RuleRec = Partial<SchemaTypes["_grist_ACLRules"]> & { id?: number, resourceRec?: ResourceRec };

type UseCB = <T>(obs: BaseObservable<T>) => T;

// Status of rules, which determines whether the "Save" button is enabled. The order of the values
// matters, as we take the max of all the parts to determine the ultimate status.
enum RuleStatus {
  Unchanged,
  ChangedValid,
  Invalid,
  CheckPending,
}

interface ISuggestionInfo {
  gristType?: string;     // If given, enables attributes using autoCompleteTypeAttributes().
  example?: string;       // Optional example value to show with suggestions.
}

// UserAttribute autocomplete choices. RuleIndex is used to filter for only those user
// attributes made available by the previous rules.
interface IAttrOption extends ISuggestionInfo {
  ruleIndex: number;
  value: string;
}

interface IColTypeInfo extends ISuggestionInfo {
  colId: string;
}

class Suggestion implements ISuggestionWithSubAttrs {
  constructor(public value: string, private _info?: ISuggestionInfo) {}
  public get example() {
    return (this._info?.gristType || "") + (this._info?.example ? ` (e.g. ${this._info?.example})` : "");
  }

  public subAttributes(): ISuggestionWithSubAttrs[] {
    if (this._info?.gristType === "Text") {
      return ["upper()", "lower()"].map(value => ({ value }));
    }
    return [];
  }
}

/**
 * Top-most container managing state and dom-building for the ACL rule UI.
 */
export class AccessRules extends Disposable {
  // Whether anything has changed, i.e. whether to show a "Save" button.
  private _ruleStatus: Computed<RuleStatus>;

  // Parsed rules obtained from DocData during last call to update(). Used for _ruleStatus.
  private _ruleCollection = new ACLRuleCollection();

  // Array of all per-table rules.
  private _tableRules = this.autoDispose(obsArray<TableRules>());
  private _sortedTableRules: Computed<TableRules[]>;

  // The default rule set for the document (for "*:*").
  private _docDefaultRuleSet = Observable.create<DefaultObsRuleSet | null>(this, null);

  // Special document-level rules, for resources of the form ("*SPECIAL:<RuleType>").
  // These rules are shown in different places - currently most are shown as a separate
  // section, and one is folded into the default rule section (for SeedRule).
  private _specialRulesWithDefault = Observable.create<SpecialRules | null>(this, null);
  private _specialRulesSeparate = Observable.create<SpecialRules | null>(this, null);
  private _specialRulesTemplates = Observable.create<SpecialRules | null>(this, null);

  // Array of all UserAttribute rules.
  private _userAttrRules = this.autoDispose(obsArray<ObsUserAttributeRule>());

  // Array of all user-attribute choices created by UserAttribute rules. Used for lookup items in
  // rules, and for ACLFormula completions.
  private _userAttrChoices: Computed<IAttrOption[]>;

  // Whether the save button should be enabled.
  private _savingEnabled: Computed<boolean>;

  // Whether to show "loading", an intro screen, or the rules UI.
  private _uiState = Observable.create<"loading" | "intro" | "rules" | "error">(this, "loading");

  // Whether to show "Disable access rules" button: when there are no custom table rules or
  // default rules (only possibly-changed special rules left).
  private _showDisableRules: Computed<boolean>;

  // Whether a save is currently in progress.
  private _saving = Observable.create(this, false);

  // Error or warning message to show next to Save/Reset buttons if non-empty.
  private _errorMessage = Observable.create(this, "");

  // Details of rule problems, for offering solutions to the user.
  private _ruleProblems = this.autoDispose(obsArray<AclRuleProblem>());

  // Map of tableId to basic metadata for all tables in the document.
  private _aclResources = new Map<string, AclTableDescription>();

  private _aclUsersPopup = ACLUsersPopup.create(this, this.gristDoc.docPageModel);

  constructor(public gristDoc: GristDoc) {
    super();
    this._ruleStatus = Computed.create(this, (use) => {
      const defRuleSet = use(this._docDefaultRuleSet);
      const tableRules = use(this._tableRules);
      const specialRulesWithDefault = use(this._specialRulesWithDefault);
      const specialRulesSeparate = use(this._specialRulesSeparate);
      const specialRulesTemplates = use(this._specialRulesTemplates);
      const userAttr = use(this._userAttrRules);
      return Math.max(
        defRuleSet ? use(defRuleSet.ruleStatus) : RuleStatus.Unchanged,
        // If any tables/userAttrs were changed or added, they will be considered changed. If
        // there were only removals, then length will be reduced.
        getChangedStatus(tableRules.length < this._ruleCollection.getAllTableIds().length),
        getChangedStatus(userAttr.length < this._ruleCollection.getUserAttributeRules().size),
        ...tableRules.map(tr => use(tr.ruleStatus)),
        ...userAttr.map(u => use(u.ruleStatus)),
        specialRulesWithDefault ? use(specialRulesWithDefault.ruleStatus) : RuleStatus.Unchanged,
        specialRulesSeparate ? use(specialRulesSeparate.ruleStatus) : RuleStatus.Unchanged,
        specialRulesTemplates ? use(specialRulesTemplates.ruleStatus) : RuleStatus.Unchanged,
      );
    });

    this._showDisableRules = Computed.create(this, (use) => {
      return Boolean(use(this._docDefaultRuleSet)?.hasOnlyBuiltInRules()) &&
        use(this._tableRules).length === 0 &&
        use(this._userAttrRules).length === 0;
    });

    this._sortedTableRules = Computed.create(this, use =>
      [...use(this._tableRules)].sort((a, b) =>
        localeCompare(a.tableId, b.tableId),
      ),
    );

    this._savingEnabled = Computed.create(this, this._ruleStatus, (use, s) =>
      (s === RuleStatus.ChangedValid));

    this._userAttrChoices = Computed.create(this, this._userAttrRules, (use, rules) => {
      // Types are Grist equivalents of corresponding fields in app/common/User.
      // Examples are also shown in autocomplete: include only the couple of commonly-used ones.
      const result: IAttrOption[] = [
        { ruleIndex: -1, value: "user.Access",     gristType: "Choice", example: "VIEWER" },
        { ruleIndex: -1, value: "user.Email",      gristType: "Text",   example: '"alice@example.com"' },
        { ruleIndex: -1, value: "user.UserID",     gristType: "Int" },
        { ruleIndex: -1, value: "user.Name",       gristType: "Text" },
        { ruleIndex: -1, value: "user.LinkKey." },
        { ruleIndex: -1, value: "user.Origin",     gristType: "Text" },
        { ruleIndex: -1, value: "user.SessionID",  gristType: "Text" },
        { ruleIndex: -1, value: "user.IsLoggedIn", gristType: "Bool",   example: "True" },
        { ruleIndex: -1, value: "user.UserRef",    gristType: "Text" },
      ];
      for (const [i, rule] of rules.entries()) {
        const tableId = use(rule.tableId);
        const name = use(rule.name);
        for (const c of this.getColTypeInfo(tableId)) {
          result.push({ ...c, ruleIndex: i, value: `user.${name}.${c.colId}` });
        }
      }
      return result;
    });

    // The UI in this module isn't really dynamic (that would be tricky while allowing unsaved
    // changes). Instead, react deliberately if rules change. Note that table/column renames would
    // trigger changes to rules, so we don't need to listen for those separately.
    for (const tableId of ["_grist_ACLResources", "_grist_ACLRules"]) {
      const tableData = this.gristDoc.docData.getTable(tableId)!;
      this.autoDispose(tableData.tableActionEmitter.addListener(this._onChange, this));
    }
    this.autoDispose(this.gristDoc.docPageModel.currentDoc.addListener(this._updateDocAccessData, this));

    this.update().catch(e => this._errorMessage.set(e.message));
  }

  public get allTableIds() { return Array.from(this._aclResources.keys()).sort(); }
  public get userAttrRules() { return this._userAttrRules; }
  public get userAttrChoices() { return this._userAttrChoices; }

  public getTableTitle(tableId: string) {
    const table = this._aclResources.get(tableId);
    if (!table) { return `#Invalid (${tableId})`; }
    return getTableTitle(table);
  }

  /**
   * Replace internal state from the rules in DocData.
   */
  public async update() {
    if (this.isDisposed()) { return; }
    this._errorMessage.set("");
    const rules = this._ruleCollection;

    try {
      const [, , aclResources] = await Promise.all([
        rules.update(this.gristDoc.docData, { log: console, pullOutSchemaEdit: true }),
        this._updateDocAccessData(),
        this.gristDoc.docComm.getAclResources(),
      ]);
      if (this.isDisposed()) { return; }

      this._aclResources = new Map(Object.entries(aclResources.tables));
      this._ruleProblems.set(aclResources.problems);
      this._uiState.set(rules.haveRules() ? "rules" : "intro");
    } catch (e) {
      // We might have failed somewhere in Promise.all
      if (this.isDisposed()) { return; }
      this._uiState.set("error");
      throw e;
    }

    this._tableRules.set(
      rules.getAllTableIds()
        .filter(tableId => (tableId !== SPECIAL_RULES_TABLE_ID))
        .map(tableId => TableRules.create(this._tableRules,
          tableId, this, rules.getAllColumnRuleSets(tableId), rules.getTableDefaultRuleSet(tableId))),
    );

    const withDefaultRules: SpecialRuleName[] = ["SeedRule"];
    const separateRules: SpecialRuleName[]  = ["SchemaEdit", "AccessRules", "DocCopies"];
    const templateRules: SpecialRuleName[]  = ["FullCopies"];

    SpecialRules.create(
      this._specialRulesWithDefault, SPECIAL_RULES_TABLE_ID, this,
      filterRuleSets(withDefaultRules, rules.getAllColumnRuleSets(SPECIAL_RULES_TABLE_ID)),
      filterRuleSet(withDefaultRules, rules.getTableDefaultRuleSet(SPECIAL_RULES_TABLE_ID)));
    SpecialRulesMain.create(
      this._specialRulesSeparate, SPECIAL_RULES_TABLE_ID, this,
      filterRuleSets(separateRules, rules.getAllColumnRuleSets(SPECIAL_RULES_TABLE_ID)),
      filterRuleSet(separateRules, rules.getTableDefaultRuleSet(SPECIAL_RULES_TABLE_ID)));
    SpecialRulesTemplates.create(
      this._specialRulesTemplates, SPECIAL_RULES_TABLE_ID, this,
      filterRuleSets(templateRules, rules.getAllColumnRuleSets(SPECIAL_RULES_TABLE_ID)),
      filterRuleSet(templateRules, rules.getTableDefaultRuleSet(SPECIAL_RULES_TABLE_ID)));
    DefaultObsRuleSet.create(this._docDefaultRuleSet, this, null, undefined, rules.getDocDefaultRuleSet());
    this._userAttrRules.set(
      Array.from(rules.getUserAttributeRules().values(), userAttr =>
        ObsUserAttributeRule.create(this._userAttrRules, this, userAttr)),
    );
  }

  /**
   * Collect the internal state into records and sync them to the document.
   */
  public async save(): Promise<void> {
    if (!this._savingEnabled.get() || this._saving.get()) {
      return;
    }

    this._saving.set(true);
    try {
      // Note that if anything has changed, we apply changes relative to the current state of the
      // ACL tables (they may have changed by other users). So our changes will win.

      const docData = this.gristDoc.docData;
      const resourcesTable = docData.getMetaTable("_grist_ACLResources");
      const rulesTable = docData.getMetaTable("_grist_ACLRules");

      // Add/remove resources to have just the ones we need.
      const newResources: MetaRowRecord<"_grist_ACLResources">[] = flatten(
        [{ tableId: "*", colIds: "*" }],
        this._specialRulesWithDefault.get()?.getResources() || [],
        this._specialRulesSeparate.get()?.getResources() || [],
        this._specialRulesTemplates.get()?.getResources() || [],
        ...this._tableRules.get().map(tr => tr.getResources()),
      )
        // Skip the fake "*SPECIAL:SchemaEdit" resource (frontend-specific); these rules are saved to the default
        // resource.
        .filter(resource => !isSchemaEditResource(resource))
        .map(r => ({ id: -1, ...r }));

      // Prepare userActions and a mapping of serializedResource to rowIds.
      const resourceSync = syncRecords(resourcesTable, newResources, serializeResource);

      const defaultResourceRowId = resourceSync.rowIdMap.get(serializeResource({ id: -1, tableId: "*", colIds: "*" }));
      if (!defaultResourceRowId) {
        throw new Error(t("Default resource missing in resource map"));
      }

      // For syncing rules, we'll go by rowId that we store with each RulePart and with the RuleSet.
      const newRules: RowRecord[] = [];
      for (const rule of this.getRules()) {
        // We use id of 0 internally to mark built-in rules. Skip those.
        if (rule.id === 0) {
          continue;
        }

        // Look up the rowId for the resource.
        let resourceRowId: number | undefined;
        // Assign the rules for the fake "*SPECIAL:SchemaEdit" resource to the default resource where they belong.
        if (isSchemaEditResource(rule.resourceRec!)) {
          resourceRowId = defaultResourceRowId;
        } else {
          const resourceKey = serializeResource(rule.resourceRec as RowRecord);
          resourceRowId = resourceSync.rowIdMap.get(resourceKey);
          if (!resourceRowId) {
            throw new Error(t("Resource missing in resource map: {{resourceKey}}", { resourceKey }));
          }
        }
        newRules.push({
          id: rule.id || -1,
          resource: resourceRowId,
          aclFormula: rule.aclFormula!,
          permissionsText: rule.permissionsText!,
          rulePos: rule.rulePos || null,
          memo: rule.memo ?? "",
        });
      }

      // UserAttribute rules are listed in the same rulesTable.
      for (const userAttr of this._userAttrRules.get()) {
        const rule = userAttr.getRule();
        newRules.push({
          id: rule.id || -1,
          resource: defaultResourceRowId,
          rulePos: rule.rulePos || null,
          userAttributes: rule.userAttributes,
        });
      }

      logTelemetryEvent("changedAccessRules", {
        full: {
          docIdDigest: this.gristDoc.docId(),
          ruleCount: newRules.length,
        },
      });

      // We need to fill in rulePos values. We'll add them in the order the rules are listed (since
      // this.getRules() returns them in a suitable order), keeping rulePos unchanged when possible.
      let lastGoodRulePos = 0;
      let lastGoodIndex = -1;
      for (let i = 0; i < newRules.length; i++) {
        const pos = newRules[i].rulePos as number;
        if (pos && pos > lastGoodRulePos) {
          const step = (pos - lastGoodRulePos) / (i - lastGoodIndex);
          for (let k = lastGoodIndex + 1; k < i; k++) {
            newRules[k].rulePos = lastGoodRulePos + step * (k - lastGoodIndex);
          }
          lastGoodRulePos = pos;
          lastGoodIndex = i;
        }
      }
      // Fill in the rulePos values for the remaining rules.
      for (let k = lastGoodIndex + 1; k < newRules.length; k++) {
        newRules[k].rulePos = ++lastGoodRulePos;
      }
      // Prepare the UserActions for syncing the Rules table.
      const rulesSync = syncRecords(rulesTable, newRules);

      // Finally collect and apply all the actions together.
      try {
        await docData.sendActions([
          ...resourceSync.userActions,
          ...rulesSync.userActions,
        ]);
      } catch (e) {
        // Report the error, but go on to update the rules. The user may lose their entries, but
        // will see what's in the document. To preserve entries and show what's wrong, we try to
        // catch errors earlier.
        reportError(e);
      }

      // Re-populate the state from DocData once the records are synced.
      if (!this.isDisposed()) {
        await this.update();
      }
    } finally {
      if (!this.isDisposed()) {
        this._saving.set(false);
      }
    }
  }

  public buildDom() {
    return dom.domComputed(this._uiState, (uiState) => {
      switch (uiState) {
        case "loading": return cssLoading(loadingSpinner(), testId("access-rules-loading"));
        case "intro": return this._buildIntro();
        case "rules": return this._buildRulesDom();
        case "error": return this._buildError();
      }
    });
  }

  public _buildRulesDom() {
    return cssOuter(
      cssAddTableRow(
        bigBasicButton({ disabled: true }, dom.hide(this._savingEnabled),
          dom.text((use) => {
            const s = use(this._ruleStatus);
            return s === RuleStatus.CheckPending ? t("Checking...") :
              s === RuleStatus.Unchanged ? t("Saved") : t("Invalid");
          }),
          testId("rules-non-save"),
        ),
        bigPrimaryButton(
          t("Save"),
          dom.show(this._savingEnabled),
          dom.on("click", () => this.save()),
          dom.prop("disabled", this._saving),
          testId("rules-save"),
        ),
        bigBasicButton(t("Reset"), dom.show(use => use(this._ruleStatus) !== RuleStatus.Unchanged),
          dom.on("click", () => this.update()),
          testId("rules-revert"),
        ),

        bigBasicButton(t("Add table rules"), cssDropdownIcon("Dropdown"), { style: "margin-left: auto" },
          menu(() =>
            this.allTableIds.map(tableId =>
              // Add the table on a timeout, to avoid disabling the clicked menu item
              // synchronously, which prevents the menu from closing on click.
              menuItemAsync(() => this._addTableRules(tableId),
                this.getTableTitle(tableId),
                dom.cls("disabled", use => use(this._tableRules).some(tr => tr.tableId === tableId)),
              ),
            ),
          ),
        ),
        bigBasicButton(t("Add user attributes"), dom.on("click", () => this._addUserAttributes())),
        bigBasicButton(t("View as"), cssDropdownIcon("Dropdown"),
          elem => this._aclUsersPopup.attachPopup(elem, { placement: "bottom-end", resetDocPage: true }),
          dom.style("visibility", use => use(this._aclUsersPopup.isInitialized) ? "" : t("hidden"))),
      ),
      cssConditionError({ style: "margin-left: 16px" },
        dom.text(this._errorMessage),
        testId("access-rules-error"),
      ),

      dom.maybe((use) => {
        const ruleProblems = use(this._ruleProblems);
        return ruleProblems.length > 0 ? ruleProblems : null;
      }, ruleProblems =>
        cssSection(
          cssRuleProblems(
            this._buildRuleProblemsDom(ruleProblems))),
      ),

      shadowScroll(
        dom.maybe(use => use(this._userAttrRules).length, () =>
          cssSection(
            cssSectionHeading(t("User Attributes")),
            cssTableRounded(
              cssTableHeaderRow(
                cssCell1(cssCell.cls("-rborder"), cssCell.cls("-center"), cssColHeaderCell("Name")),
                cssCell4(
                  cssColumnGroup(
                    cssCell1(cssColHeaderCell(t("Attribute to Look Up"))),
                    cssCell1(cssColHeaderCell(t("Lookup Table"))),
                    cssCell1(cssColHeaderCell(t("Lookup Column"))),
                    cssCellIcon(),
                  ),
                ),
              ),
              dom.forEach(this._userAttrRules, userAttr => userAttr.buildUserAttrDom()),
            ),
          ),
        ),
        dom.forEach(this._sortedTableRules, tableRules => tableRules.buildDom()),
        cssSection(
          cssSectionHeading(t("Default rules"), testId("rule-table-header")),
          dom.maybe(this._specialRulesWithDefault, tableRules => cssSeedRule(
            tableRules.buildCheckBoxes())),
          cssTableRounded(
            cssTableHeaderRow(
              cssCell1(cssCell.cls("-rborder"), cssCell.cls("-center"), cssColHeaderCell(t("Columns"))),
              cssCell4(
                cssColumnGroup(
                  cssCellIcon(),
                  cssCell2(cssColHeaderCell(t("Condition"))),
                  cssCell1(cssColHeaderCell(t("Permissions"))),
                  cssCellIconWithMargins(),
                  cssCellIcon(),
                ),
              ),
            ),
            dom.maybe(this._docDefaultRuleSet, ruleSet => ruleSet.buildRuleSetDom()),
          ),
          testId("rule-table"),
        ),
        dom.maybe(this._specialRulesSeparate, tableRules => tableRules.buildDom()),
        dom.maybe(this._specialRulesTemplates, tableRules => tableRules.buildDom()),
        dom.maybe(this._showDisableRules, () =>
          cssDisableRulesButton(icon("Remove"), t("Disable Access Rules"),
            dom.on("click", () => this._confirmDisableAccessRules()),
            testId("disable-access-rules"),
          ),
        ),
      ),
    );
  }

  public _buildRuleProblemsDom(ruleProblems: AclRuleProblem[]) {
    const buttons: (HTMLAnchorElement | HTMLButtonElement)[] = [];
    for (const problem of ruleProblems) {
      // Is the problem a missing table?
      if (problem.tables) {
        this._addButtonsForMissingTables(buttons, problem.tables.tableIds);
      }
      // Is the problem a missing column?
      if (problem.columns) {
        this._addButtonsForMissingColumns(buttons, problem.columns.tableId, problem.columns.colIds);
      }
      // Is the problem a misconfigured user attribute?
      if (problem.userAttributes) {
        const names = problem.userAttributes.names;
        this._addButtonsForMisconfiguredUserAttributes(buttons, names);
      }
    }
    return buttons.map(button => dom("span", button));
  }

  /**
   * Get a list of all rule records, for saving.
   */
  public getRules(): RuleRec[] {
    return flatten(
      ...this._tableRules.get().map(tr => tr.getRules()),
      this._specialRulesWithDefault.get()?.getRules() || [],
      this._specialRulesSeparate.get()?.getRules() || [],
      this._specialRulesTemplates.get()?.getRules() || [],
      this._docDefaultRuleSet.get()?.getRules("*") || [],
    );
  }

  public removeTableRules(tableRules: TableRules) {
    removeItem(this._tableRules, tableRules);
  }

  public removeUserAttributes(userAttr: ObsUserAttributeRule) {
    removeItem(this._userAttrRules, userAttr);
  }

  public async checkAclFormula(text: string): Promise<PredicateFormulaProperties> {
    if (text) {
      return this.gristDoc.docComm.checkAclFormula(text);
    }
    return {};
  }

  // Check if the given tableId, and optionally a list of colIds, are present in this document.
  // Returns '' if valid, or an error string if not. Exempt colIds will not trigger an error.
  public checkTableColumns(tableId: string, colIds?: string[], exemptColIds?: string[]): string {
    if (!tableId || tableId === SPECIAL_RULES_TABLE_ID) { return ""; }
    const tableColIds = this._aclResources.get(tableId)?.colIds;
    if (!tableColIds) { return t("Invalid table: {{tableId}}", { tableId }); }
    if (colIds) {
      const validColIds = new Set([...tableColIds, ...exemptColIds || []]);
      const invalidColIds = colIds.filter(c => !validColIds.has(c));
      if (invalidColIds.length === 0) { return ""; }
      return t(
        "Invalid columns in table {{tableId}}: {{invalidColIds}}",
        { tableId, invalidColIds: invalidColIds.join(", ") },
      );
    }
    return "";
  }

  // Returns a list of valid colIds for the given table, or undefined if the table isn't valid.
  public getValidColIds(tableId: string): string[] | undefined {
    return this._aclResources.get(tableId)?.colIds.filter(id => !isHiddenCol(id)).sort();
  }

  public getColTypeInfo(tableId?: string): IColTypeInfo[] {
    if (!tableId) { return []; }
    return getColTypeInfo(this.getValidColIds(tableId) || [], this.gristDoc.docData.getTable(tableId));
  }

  public typeCheckFormula(formulaParsed: ParsedPredicateFormula, tableId?: string): string | false {
    const sampleRecord = tableId ? this._getSampleRecord(tableId) : new EmptyRecordView();

    const userAttrSamples: { [key: string]: InfoView } = {};
    for (const attr of this._userAttrRules.get()) {
      userAttrSamples[attr.name.get()] = this._getSampleRecord(attr.tableId.get());
    }
    return typeCheckFormula(formulaParsed, sampleRecord, userAttrSamples);
  }

  // Get rules to use for seeding any new set of table/column rules, e.g. to give owners
  // broad rights over the table/column contents.
  public getSeedRules(): ObsRulePart[] {
    return this._specialRulesWithDefault.get()?.getCustomRules("SeedRule") || [];
  }

  private _buildError() {
    return cssIntroSection(
      markdown(t(`\
## Access Rules

You don't have permission to view or edit access rules for this document.`,
      )),
      cssErrorDetails("(Error: ", dom.text(this._errorMessage), ")"),
    );
  }

  private _buildIntro() {
    return cssIntroSection(cssNestedLinks(
      markdown(t(`\
## Access Rules

Basic access to this document is controlled using the 'Manage Users' option in the 'Share' \
menu, where you can assign collaborator roles such as Owner, Editor, or Viewer.

For more granular control, you can create Access Rules to limit who can view or edit specific
tables, columns, or rows — useful for sensitive data or role-based permissions.
[Learn more.]({{helpAccessRules}})`,
      { helpAccessRules: commonUrls.helpAccessRules }),
      ),
      cssIntroButton(
        bigPrimaryButton("Enable Access Rules",
          dom.on("click", () => this._confirmEnableAccessRules()),
          testId("enable-access-rules"),
        ),
      ),
      testId("access-rules-intro"),
    ));
  }

  private _confirmEnableAccessRules() {
    confirmModal(
      t("Enable Access Rules"),
      t("Continue"),
      () => this._doEnableAccessRules(),
      {
        explanation: markdown(t(`\
After enabling Access Rules, Editors will no longer be able to change the structure of the
document or edit formulas. Only Owners will be able to copy or download the document.

These settings can be changed under 'Special rules'.`,
        )),
      },
    );
  }

  private _doEnableAccessRules() {
    this._specialRulesSeparate.get()?.setToRecommended();
    this._uiState.set("rules");
  }

  private _confirmDisableAccessRules() {
    confirmModal(
      t("Disable Access Rules"),
      t("Disable and save"),
      () => this._doDisableAccessRules(),
      {
        explanation: markdown(t(`\
After disabling Access Rules, Editors will be able to change the structure of the document \
and edit formulas. Editors and Viewers will be able to see all data in the document, \
as well as copy or download it.`,
        )),
      },
    );
  }

  private _doDisableAccessRules() {
    if (!this._showDisableRules.get()) {
      // We should only be able to get here if there are no custom rules other than the special
      // checkboxes. If we do, that's a sign of a bug.
      throw new UserError("Clear existing custom rules first");
    }
    this._specialRulesWithDefault.get()?.clearRules();
    this._specialRulesSeparate.get()?.clearRules();
    this._specialRulesTemplates.get()?.clearRules();
    return this.save();
  }

  private _getSampleRecord(tableId: string): InfoView {
    return getSampleRecord(this.getValidColIds(tableId) || [], this.gristDoc.docData.getTable(tableId));
  }

  private _addTableRules(tableId: string) {
    if (this._tableRules.get().some(tr => tr.tableId === tableId)) {
      throw new Error(t("Trying to add TableRules for existing table {{tableId}}", { tableId }));
    }
    const defRuleSet: RuleSet = { tableId, colIds: "*", body: [] };
    const tableRules = TableRules.create(this._tableRules, tableId, this, undefined, defRuleSet);
    this._tableRules.push(tableRules);
    tableRules.addDefaultRules(this.getSeedRules());
  }

  private _addUserAttributes() {
    this._userAttrRules.push(ObsUserAttributeRule.create(this._userAttrRules, this, undefined, { focus: true }));
  }

  private _onChange() {
    if (this._ruleStatus.get() === RuleStatus.Unchanged) {
      // If no changes, it's safe to just reload the rules from docData.
      this.update().catch(e => this._errorMessage.set(e.message));
    } else {
      this._errorMessage.set(
        t("Access rules have changed. Click Reset to revert your changes and refresh the rules."),
      );
    }
  }

  private async _updateDocAccessData() {
    await this._aclUsersPopup.load();
  }

  private _addButtonsForMissingTables(buttons: (HTMLAnchorElement | HTMLButtonElement)[], tableIds: string[]) {
    for (const tableId of tableIds) {
      // We don't know what the table's name was, just its tableId.
      // Hopefully, the user will understand.
      const title = t("Remove {{- tableId }} rules", { tableId });
      const button = bigBasicButton(title, cssRemoveIcon("Remove"), dom.on("click", async () => {
        this._tableRules.get()
          .filter(rules => rules.tableId === tableId)
          .forEach(rules => rules.remove());
        button.style.display = "none";
      }));
      buttons.push(button);
    }
  }

  private _addButtonsForMissingColumns(buttons: (HTMLAnchorElement | HTMLButtonElement)[],
    tableId: string, colIds: string[]) {
    const removeColRules = (rules: TableRules, colId: string) => {
      for (const rule of rules.columnRuleSets.get()) {
        const ruleColIds = new Set(rule.getColIdList());
        if (!ruleColIds.has(colId)) { continue; }
        if (ruleColIds.size === 1) {
          rule.remove();
        } else {
          rule.removeColId(colId);
        }
      }
    };
    for (const colId of colIds) {
      // TODO: we could translate tableId to table name in this case.
      const title = t("Remove column {{- colId }} from {{- tableId }} rules", { tableId, colId });
      const button = bigBasicButton(title, cssRemoveIcon("Remove"), dom.on("click", async () => {
        this._tableRules.get()
          .filter(rules => rules.tableId === tableId)
          .forEach(rules => removeColRules(rules, colId));
        button.style.display = "none";
      }));
      buttons.push(button);
    }
  }

  private _addButtonsForMisconfiguredUserAttributes(
    buttons: (HTMLAnchorElement | HTMLButtonElement)[],
    names: string[],
  ) {
    for (const name of names) {
      const title = t("Remove {{- name }} user attribute", { name });
      const button = bigBasicButton(title, cssRemoveIcon("Remove"), dom.on("click", async () => {
        this._userAttrRules.get()
          .filter(rule => rule.name.get() === name)
          .forEach(rule => rule.remove());
        button.style.display = "none";
      }));
      buttons.push(button);
    }
  }
}

// Represents all rules for a table.
class TableRules extends Disposable {
  // Whether any table rules changed, and if they are valid.
  public ruleStatus: Computed<RuleStatus>;

  // The column-specific rule sets.
  protected _columnRuleSets = this.autoDispose(obsArray<ColumnObsRuleSet>());

  // Whether there are any column-specific rule sets.
  private _haveColumnRules = Computed.create(this, this._columnRuleSets, (use, cols) => cols.length > 0);

  // The default rule set (for columns '*'), if one is set.
  private _defaultRuleSet = Observable.create<DefaultObsRuleSet | null>(this, null);

  constructor(public readonly tableId: string, public _accessRules: AccessRules,
    private _colRuleSets?: RuleSet[], private _defRuleSet?: RuleSet) {
    super();
    this._columnRuleSets.set(this._colRuleSets?.map(rs =>
      this._createColumnObsRuleSet(this._columnRuleSets, rs,
        rs.colIds === "*" ? [] : rs.colIds)) || []);

    if (!this._colRuleSets) {
      // Must be a newly-created TableRules object. Just create a default RuleSet (for tableId:*)
      DefaultObsRuleSet.create(this._defaultRuleSet, this._accessRules, this, this._haveColumnRules);
    } else if (this._defRuleSet) {
      DefaultObsRuleSet.create(this._defaultRuleSet, this._accessRules, this, this._haveColumnRules,
        this._defRuleSet);
    }

    this.ruleStatus = Computed.create(this, (use) => {
      const columnRuleSets = use(this._columnRuleSets);
      const d = use(this._defaultRuleSet);
      return Math.max(
        getChangedStatus(
          !this._colRuleSets ||                               // This TableRules object must be newly-added
          Boolean(d) !== Boolean(this._defRuleSet) ||         // Default rule set got added or removed
          columnRuleSets.length < this._colRuleSets.length,    // There was a removal
        ),
        d ? use(d.ruleStatus) : RuleStatus.Unchanged,         // Default rule set got changed.
        ...columnRuleSets.map(rs => use(rs.ruleStatus)));     // Column rule set was added or changed.
    });
  }

  /**
   * Get all custom rules for the specific column. Used to gather the current
   * setting of a special rule. Returns an empty list for unknown columns.
   */
  public getCustomRules(colId: string): ObsRulePart[] {
    for (const ruleSet of this._columnRuleSets.get()) {
      if (ruleSet.getColIds() === colId) {
        return ruleSet.getCustomRules();
      }
    }
    return [];
  }

  /**
   * Add the provided rules, copying their formula, permissions, and memo.
   */
  public addDefaultRules(rules: ObsRulePart[]) {
    const ruleSet = this._defaultRuleSet.get();
    ruleSet?.addRuleParts(rules, { foldEveryoneRule: true });
  }

  public remove() {
    this._accessRules.removeTableRules(this);
  }

  public get columnRuleSets() {
    return this._columnRuleSets;
  }

  public buildDom() {
    return cssSection(
      cssSectionHeading(
        dom("span", t("Rules for table "), cssTableName(this._accessRules.getTableTitle(this.tableId))),
        cssIconButton(icon("Dots"), { style: "margin-left: auto" },
          menu(() => [
            menuItemAsync(() => this._addColumnRuleSet(), t("Add column rule")),
            menuItemAsync(() => this._addDefaultRuleSet(), t("Add table-wide rule")),
            menuItemAsync(() => this._accessRules.removeTableRules(this), t("Delete table rules")),
          ]),
          testId("rule-table-menu-btn"),
        ),
        testId("rule-table-header"),
      ),
      cssTableRounded(
        cssTableHeaderRow(
          cssCell1(cssCell.cls("-rborder"), cssCell.cls("-center"), cssColHeaderCell(t("Columns"))),
          cssCell4(
            cssColumnGroup(
              cssCellIcon(),
              cssCell2(cssColHeaderCell(t("Condition"))),
              cssCell1(cssColHeaderCell(t("Permissions"))),
              cssCellIconWithMargins(),
              cssCellIcon(),
            ),
          ),
        ),
        this.buildColumnRuleSets(),
      ),
      this.buildErrors(),
      testId("rule-table"),
    );
  }

  public buildColumnRuleSets() {
    return [
      dom.forEach(this._columnRuleSets, ruleSet => ruleSet.buildRuleSetDom()),
      dom.maybe(this._defaultRuleSet, ruleSet => ruleSet.buildRuleSetDom()),
    ];
  }

  public buildErrors() {
    return dom.forEach(this._columnRuleSets, c => cssConditionError(dom.text(c.formulaError)));
  }

  /**
   * Return the resources (tableId:colIds entities), for saving, checking along the way that they
   * are valid.
   */
  public getResources(): ResourceRec[] {
    // Check that the colIds are valid.
    const seen = {
      allow: new Set<string>(),   // columns mentioned in rules that only have 'allow's.
      deny: new Set<string>(),    // columns mentioned in rules that only have 'deny's.
      mixed: new Set<string>(),    // columns mentioned in any rules.
    };
    for (const ruleSet of this._columnRuleSets.get()) {
      const sign = ruleSet.summarizePermissions();
      const counterSign = sign === "mixed" ? "mixed" : (sign === "allow" ? "deny" : "allow");
      const colIds = ruleSet.getColIdList();
      if (colIds.length === 0) {
        throw new UserError(t("No columns listed in a column rule for table {{tableId}}", { tableId: this.tableId }));
      }
      for (const colId of colIds) {
        if (seen[counterSign].has(colId)) {
          // There may be an order dependency between rules.  We've done a little analysis, to
          // allow the useful pattern of forbidding all access to columns, and then adding back
          // access to different sets for different teams/conditions (or allowing all access
          // by default, and then forbidding different sets).  But if there's a mix of
          // allows and denies, then we throw up our hands.
          // TODO: could analyze more deeply.  An easy step would be to analyze per permission bit.
          // Could also allow order dependency and provide a way to control the order.
          // TODO: could be worth also flagging multiple rulesets with the same columns as
          // undesirable.
          throw new UserError(
            t("Column {{colId}} appears in multiple rules for table {{tableId}} \
that might be order-dependent. Try splitting rules up differently?", { colId, tableId: this.tableId },
            ),
          );
        }
        if (sign === "mixed") {
          seen.allow.add(colId);
          seen.deny.add(colId);
          seen.mixed.add(colId);
        } else {
          seen[sign].add(colId);
          seen.mixed.add(colId);
        }
      }
    }

    return [
      ...this._columnRuleSets.get().map(rs => ({ tableId: this.tableId, colIds: rs.getColIds() })),
      { tableId: this.tableId, colIds: "*" },
    ];
  }

  /**
   * Get rules for this table, for saving.
   */
  public getRules(): RuleRec[] {
    return flatten(
      ...this._columnRuleSets.get().map(rs => rs.getRules(this.tableId)),
      this._defaultRuleSet.get()?.getRules(this.tableId) || [],
    );
  }

  public removeRuleSet(ruleSet: ObsRuleSet) {
    if (ruleSet === this._defaultRuleSet.get()) {
      this._defaultRuleSet.set(null);
    } else {
      removeItem(this._columnRuleSets, ruleSet);
    }
    if (!this._defaultRuleSet.get() && this._columnRuleSets.get().length === 0) {
      this._accessRules.removeTableRules(this);
    }
  }

  protected _createColumnObsRuleSet(
    owner: IDisposableOwner,
    ruleSet: RuleSet | undefined, initialColIds: string[],
  ): ColumnObsRuleSet {
    return ColumnObsRuleSet.create(owner, this._accessRules, this, ruleSet, initialColIds);
  }

  private _addColumnRuleSet() {
    const ruleSet = ColumnObsRuleSet.create(this._columnRuleSets, this._accessRules, this, undefined, []);
    this._columnRuleSets.push(ruleSet);
    ruleSet.addRuleParts(this._accessRules.getSeedRules(), { foldEveryoneRule: true });
  }

  private _addDefaultRuleSet() {
    const ruleSet = this._defaultRuleSet.get();
    if (!ruleSet) {
      DefaultObsRuleSet.create(this._defaultRuleSet, this._accessRules, this, this._haveColumnRules);
      this.addDefaultRules(this._accessRules.getSeedRules());
    } else {
      const part = ruleSet.addRulePart(ruleSet.getDefaultCondition());
      setTimeout(() => part.focusEditor?.(), 0);
    }
  }
}

class SpecialRules extends TableRules {
  public buildDom() {
    return cssSection(
      cssSectionHeadingMarkdown(
        dom("span", inlineMarkdown(t("**Special rules** (expand each rule to customize who it applies to)"))),
        testId("rule-table-header"),
      ),
      this.buildCheckBoxes(),
      testId("rule-table"),
    );
  }

  // Build dom with checkboxes, without a section wrapping it.
  // Used for folding a special rule into another section.
  public buildCheckBoxes() {
    return [
      this.buildColumnRuleSets(),
      this.buildErrors(),
    ];
  }

  public getResources(): ResourceRec[] {
    return this._columnRuleSets.get()
      .filter(rs => !rs.hasOnlyBuiltInRules())
      .map(rs => ({ tableId: this.tableId, colIds: rs.getColIds() }));
  }

  public setToRecommended() {
    for (const colRuleSet of this.columnRuleSets.get()) {
      if (colRuleSet instanceof SpecialObsRuleSet) {
        colRuleSet.setToRecommended();
      }
    }
  }

  public clearRules() {
    for (const colRuleSet of this.columnRuleSets.get()) {
      if (colRuleSet instanceof SpecialObsRuleSet) {
        colRuleSet.clearRules();
      }
    }
  }

  protected _createColumnObsRuleSet(
    owner: IDisposableOwner,
    ruleSet: RuleSet | undefined, initialColIds: string[],
  ): ColumnObsRuleSet {
    return SpecialObsRuleSet.create(owner, this._accessRules, this, ruleSet, initialColIds);
  }
}

class SpecialRulesMain extends SpecialRules {
  private _denyCopies?: SpecialObsRuleSetDenyCopies;
  private _allowAccessRules?: SpecialObsRuleSetAccessRules;

  public get allowAccessRules() { return this._allowAccessRules; }

  public onAccessRulesToggled(value: boolean) {
    this._denyCopies?.onAccessRulesToggled(value);
  }

  protected _createColumnObsRuleSet(
    owner: IDisposableOwner,
    ruleSet: RuleSet | undefined, initialColIds: string[],
  ): ColumnObsRuleSet {
    if (isEqual(ruleSet?.colIds, ["SchemaEdit"])) {
      // The special rule for "schemaEdit" permissions.
      return SpecialSchemaObsRuleSet.create(owner, this._accessRules, this, ruleSet, initialColIds);
    } else if (isEqual(ruleSet?.colIds, ["AccessRules"])) {
      return (this._allowAccessRules =
        SpecialObsRuleSetAccessRules.create(owner, this._accessRules, this, ruleSet, initialColIds));
    } else if (isEqual(ruleSet?.colIds, ["DocCopies"])) {
      return (this._denyCopies =
        SpecialObsRuleSetDenyCopies.create(owner, this._accessRules, this, ruleSet, initialColIds));
    } else {
      return SpecialObsRuleSet.create(owner, this._accessRules, this, ruleSet, initialColIds);
    }
  }
}

class SpecialRulesTemplates extends SpecialRules {
  private _isExpanded = Observable.create<boolean>(this, this.getResources().length > 0);

  public buildDom() {
    return cssSection(
      cssSectionHeading(
        cssSectionHeadingToggle(cssSectionHeadingToggleIcon("Expand"),
          cssSectionHeadingToggle.cls("-expanded", this._isExpanded),
          dom.on("click", () => this._isExpanded.set(!this._isExpanded.get())),
          testId("special-rules-templates-expand"),
        ),
        t("Special rules for templates"),
      ),
      dom.maybe(this._isExpanded, () => this.buildCheckBoxes()),
      testId("special-rules-templates"),
    );
  }
}

// Represents one RuleSet, for a combination of columns in one table, or the default RuleSet for
// all remaining columns in a table.
abstract class ObsRuleSet extends Disposable {
  // Whether rules changed, and if they are valid. Never unchanged if this._ruleSet is undefined.
  public ruleStatus: Computed<RuleStatus>;

  // List of individual rule parts for this entity. The default permissions may be included as the
  // last rule part, with an empty aclFormula.
  protected readonly _body = this.autoDispose(obsArray<ObsRulePart>());

  // ruleSet is omitted for a new ObsRuleSet added by the user.
  constructor(public accessRules: AccessRules, protected _tableRules: TableRules | null, private _ruleSet?: RuleSet) {
    super();
    const parts = this._ruleSet?.body.map(part => ObsRulePart.create(this._body, this, part)) || [];
    if (parts.length === 0) {
      // If creating a new RuleSet, or if there are no rules,
      // start with just a default permission part.
      parts.push(ObsRulePart.create(this._body, this, undefined));
    }
    this._body.set(parts);

    this.ruleStatus = Computed.create(this, this._body, (use, body) => {
      // If anything was changed or added, some part.ruleStatus will be other than Unchanged. If
      // there were only removals, then body.length will have changed.
      // Ignore empty rules.
      return Math.max(
        getChangedStatus(body.filter(part => !part.isEmpty(use)).length < (this._ruleSet?.body?.length || 0)),
        ...body.map(part => use(part.ruleStatus)));
    });
  }

  public remove() {
    this._tableRules?.removeRuleSet(this);
  }

  public getRules(tableId: string): RuleRec[] {
    // Return every part in the body, tacking on resourceRec to each rule.
    return this._body.get().map(part => ({
      ...part.getRulePart(),
      resourceRec: { tableId, colIds: this.getColIds() },
    }))
    // Skip entirely empty rule parts: they are invalid and dropping them is the best fix.
      .filter(part => part.aclFormula || part.permissionsText);
  }

  public getColIds(): string {
    return "*";
  }

  /**
   * Check if RuleSet may only add permissions, only remove permissions, or may do either.
   * A rule that neither adds nor removes permissions is treated as mixed for simplicity,
   * though this would be suboptimal if this were a useful case to support.
   */
  public summarizePermissions(): MixedPermissionValue {
    return summarizePermissions(this._body.get().map(p => p.summarizePermissions()));
  }

  public abstract buildResourceDom(): DomElementArg;

  public buildRuleSetDom() {
    return cssTableRow(
      cssCell1(cssCell.cls("-rborder"),
        this.buildResourceDom(),
        testId("rule-resource"),
      ),
      cssCell4(cssRuleBody.cls(""),
        dom.forEach(this._body, part => part.buildRulePartDom()),
        dom.maybe(use => !this.hasDefaultCondition(use), () =>
          cssColumnGroup(
            { style: "min-height: 28px" },
            cssCellIcon(
              cssIconButton(icon("Plus"),
                dom.on("click", () => this.addRulePart(null)),
                testId("rule-add"),
              ),
            ),
            testId("rule-extra-add"),
          ),
        ),
      ),
      testId("rule-set"),
    );
  }

  public removeRulePart(rulePart: ObsRulePart) {
    removeItem(this._body, rulePart);
    if (this._body.get().length === 0) {
      this._tableRules?.removeRuleSet(this);
    }
  }

  public addRulePart(beforeRule: ObsRulePart | null,
    content?: RulePart,
    isNew: boolean = false): ObsRulePart {
    const body = this._body.get();
    const i = beforeRule ? body.indexOf(beforeRule) : body.length;
    const part = ObsRulePart.create(this._body, this, content, isNew);
    this._body.splice(i, 0, part);
    return part;
  }

  /**
   * Add a sequence of rules, taking priority over existing rules.
   * optionally, if lowest-priority rule being added applies to
   * everyone, and the existing rule also applies to everyone,
   * fold those rules into one.
   * This method is currently only called on newly created rule
   * sets, so there's no need to check permissions and memos.
   */
  public addRuleParts(newParts: ObsRulePart[], options: { foldEveryoneRule?: boolean }) {
    // Check if we need to consider folding rules that apply to everyone.
    if (options.foldEveryoneRule) {
      const oldParts = this._body.get();
      const myEveryonePart = (oldParts.length === 1 && !oldParts[0].getRulePart().aclFormula) ? oldParts[0] : null;
      const newEveryonePart = newParts[newParts.length - 1]?.getRulePart().aclFormula ? null :
        newParts[newParts.length - 1];
      if (myEveryonePart && newEveryonePart) {
        // It suffices to remove the existing rule that applies to everyone,
        // which is just an empty default from rule set creation.
        removeItem(this._body, myEveryonePart);
      }
    }
    for (const part of [...newParts].reverse()) {
      const { permissionsText, aclFormula, memo } = part.getRulePart();
      if (permissionsText === undefined || aclFormula === undefined) {
        // Should not happen.
        continue;
      }

      // Include only the permissions for the bits that this RuleSet supports. E.g. this matters
      // for seed rules, which may include create/delete bits which shouldn't apply to columns.
      const origPermissions = parsePermissions(permissionsText);
      const trimmedPermissions = trimPermissions(origPermissions, this.getAvailableBits());
      const trimmedPermissionsText = permissionSetToText(trimmedPermissions);

      this.addRulePart(
        this.getFirst() || null,
        {
          aclFormula,
          permissionsText: trimmedPermissionsText,
          permissions: trimmedPermissions,
          memo,
        },
        true,
      );
    }
  }

  /**
   * Returns the first built-in rule. It's the only one of the built-in rules to get a "+" next to
   * it, since we don't allow inserting new rules in-between built-in rules.
   */
  public getFirstBuiltIn(): ObsRulePart | undefined {
    return this._body.get().find(p => p.isBuiltIn());
  }

  // Get first rule part, built-in or not.
  public getFirst(): ObsRulePart | undefined {
    return this._body.get()[0];
  }

  /**
   * When an empty-condition RulePart is the only part of a RuleSet, we can say it applies to
   * "Everyone".
   */
  public isSoleCondition(use: UseCB, part: ObsRulePart): boolean {
    const body = use(this._body);
    return body.length === 1 && body[0] === part;
  }

  /**
   * When an empty-condition RulePart is last in a RuleSet, we say it applies to "Everyone Else".
   */
  public isLastCondition(use: UseCB, part: ObsRulePart): boolean {
    const body = use(this._body);
    return body[body.length - 1] === part;
  }

  public hasDefaultCondition(use: UseCB): boolean {
    const body = use(this._body);
    return body.length > 0 && body[body.length - 1].hasEmptyCondition(use);
  }

  public getDefaultCondition(): ObsRulePart | null {
    const body = this._body.get();
    const last = body.length > 0 ? body[body.length - 1] : null;
    return last?.hasEmptyCondition(unwrap) ? last : null;
  }

  /**
   * Which permission bits to allow the user to set.
   */
  public getAvailableBits(): PermissionKey[] {
    return AVAILABLE_BITS_TABLES;
  }

  /**
   * Get valid colIds for the table that this RuleSet is for.
   */
  public getValidColIds(): string[] {
    const tableId = this._tableRules?.tableId;
    return (tableId && this.accessRules.getValidColIds(tableId)) || [];
  }

  public getColTypeInfo() { return this.accessRules.getColTypeInfo(this._tableRules?.tableId); }

  public typeCheckFormula(formulaParsed: ParsedPredicateFormula) {
    return this.accessRules.typeCheckFormula(formulaParsed, this._tableRules?.tableId);
  }

  /**
   * Check if this rule set is limited to a set of columns.
   */
  public hasColumns() {
    return false;
  }

  public hasOnlyBuiltInRules() {
    return this._body.get().every(rule => rule.isBuiltIn());
  }

  // Get rule parts that are neither built-in nor empty.
  public getCustomRules(): ObsRulePart[] {
    return this._body.get().filter(rule => !rule.isBuiltInOrEmpty());
  }

  /**
   * If the set applies to a special column, return its name.
   */
  public getSpecialColumn(): string | undefined {
    if (this._ruleSet?.tableId === SPECIAL_RULES_TABLE_ID &&
      this._ruleSet.colIds.length === 1) {
      return this._ruleSet.colIds[0];
    }
  }
}

class ColumnObsRuleSet extends ObsRuleSet {
  // Error message for this rule set, or '' if valid.
  public formulaError: Computed<string>;

  private _colIds = Observable.create<string[]>(this, this._initialColIds);

  constructor(accessRules: AccessRules, tableRules: TableRules, ruleSet: RuleSet | undefined,
    private _initialColIds: string[]) {
    super(accessRules, tableRules, ruleSet);

    this.formulaError = Computed.create(this, (use) => {
      // Exempt existing colIds from checks, by including as a third argument.
      return accessRules.checkTableColumns(tableRules.tableId, use(this._colIds), this._initialColIds);
    });

    const baseRuleStatus = this.ruleStatus;
    this.ruleStatus = Computed.create(this, (use) => {
      if (use(this.formulaError)) { return RuleStatus.Invalid; }
      return Math.max(
        getChangedStatus(!isEqual(use(this._colIds), this._initialColIds)),
        use(baseRuleStatus));
    });
  }

  public buildResourceDom(): DomElementArg {
    return aclColumnList(this._colIds, this._getValidColIdsList());
  }

  public getColIdList(): string[] {
    return this._colIds.get();
  }

  public removeColId(colId: string) {
    this._colIds.set(this._colIds.get().filter(c => (c !== colId)));
  }

  public getColIds(): string {
    return this._colIds.get().join(",");
  }

  public getAvailableBits(): PermissionKey[] {
    return AVAILABLE_BITS_COLUMNS;
  }

  public hasColumns() {
    return true;
  }

  private _getValidColIdsList(): string[] {
    return this.getValidColIds().filter(id => id !== "id");
  }
}

class DefaultObsRuleSet extends ObsRuleSet {
  constructor(accessRules: AccessRules, tableRules: TableRules | null,
    private _haveColumnRules?: Observable<boolean>, ruleSet?: RuleSet) {
    super(accessRules, tableRules, ruleSet);
  }

  public buildResourceDom() {
    return [
      cssCenterContent.cls(""),
      cssDefaultLabel(
        dom.domComputed(use => this._haveColumnRules && use(this._haveColumnRules), haveColRules =>
          haveColRules ? withInfoTooltip(t("All"), "accessRulesTableWide") : t("All")),
      ),
    ];
  }
}

interface SpecialRuleBody {
  permissions: string;
  formula: string;
}

/**
 * Properties we need to know about how a special rule should function and
 * be rendered.
 */
interface SpecialRuleProperties extends SpecialRuleBody {
  description: string;
  name: string;
  availableBits: PermissionKey[];
}

const schemaEditRules: { [key: string]: SpecialRuleBody } = {
  allowEditors: {
    permissions: "+S",
    formula: "user.Access == EDITOR",
  },
  denyEditors: {
    permissions: "-S",
    formula: "user.Access != OWNER",
  },
};

const specialRuleProperties: Record<SpecialRuleName, SpecialRuleProperties> = {
  Ac
Download .txt
gitextract_r_s8ij64/

├── .dockerignore
├── .editorconfig
├── .git-blame-ignore-revs
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── 00-bug-issue.yml
│   │   ├── 10-installation-issue.yml
│   │   ├── 20-feature-request.yml
│   │   └── config.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── cla/
│   │   ├── individual-cla.md
│   │   └── signatures.json
│   └── workflows/
│       ├── cla.yml
│       ├── docker.yml
│       ├── docker_latest.yml
│       ├── fly-build.yml
│       ├── fly-cleanup.yml
│       ├── fly-deploy.yml
│       ├── fly-destroy.yml
│       ├── main.yml
│       ├── self-hosted.yml
│       └── translation_keys.yml
├── .gitignore
├── .nvmrc
├── .yarnrc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE.txt
├── NOTICE.txt
├── README.md
├── SECURITY.md
├── app/
│   ├── cli.sh
│   ├── client/
│   │   ├── DefaultHooks.ts
│   │   ├── Hooks.ts
│   │   ├── aclui/
│   │   │   ├── ACLColumnList.ts
│   │   │   ├── ACLFormulaEditor.ts
│   │   │   ├── ACLMemoEditor.ts
│   │   │   ├── ACLSelect.ts
│   │   │   ├── ACLUsers.ts
│   │   │   ├── AccessRules.ts
│   │   │   └── PermissionsWidget.ts
│   │   ├── apiconsole.ts
│   │   ├── app.css
│   │   ├── app.js
│   │   ├── billingMain.ts
│   │   ├── browserCheck.ts
│   │   ├── components/
│   │   │   ├── AceEditor.css
│   │   │   ├── AceEditor.js
│   │   │   ├── AceEditorCompletions.ts
│   │   │   ├── ActionCounter.ts
│   │   │   ├── ActionLog.css
│   │   │   ├── ActionLog.ts
│   │   │   ├── Banner.ts
│   │   │   ├── BaseView.ts
│   │   │   ├── BaseView2.ts
│   │   │   ├── BehavioralPromptsManager.ts
│   │   │   ├── CellPosition.ts
│   │   │   ├── CellSelector.ts
│   │   │   ├── ChartView.css
│   │   │   ├── ChartView.ts
│   │   │   ├── ClientScope.ts
│   │   │   ├── Clipboard.css
│   │   │   ├── Clipboard.ts
│   │   │   ├── CodeEditorPanel.css
│   │   │   ├── CodeEditorPanel.ts
│   │   │   ├── ColumnFilters.css
│   │   │   ├── ColumnTransform.ts
│   │   │   ├── Comm.ts
│   │   │   ├── CopySelection.ts
│   │   │   ├── CoreBanners.ts
│   │   │   ├── Cursor.ts
│   │   │   ├── CursorMonitor.ts
│   │   │   ├── CustomCalendarView.ts
│   │   │   ├── CustomView.css
│   │   │   ├── CustomView.ts
│   │   │   ├── DataTables.ts
│   │   │   ├── DetailView.css
│   │   │   ├── DetailView.ts
│   │   │   ├── DocComm.ts
│   │   │   ├── DocumentUsage.ts
│   │   │   ├── Drafts.ts
│   │   │   ├── DropdownConditionConfig.ts
│   │   │   ├── DropdownConditionEditor.ts
│   │   │   ├── EditorMonitor.ts
│   │   │   ├── EmbedForm.css
│   │   │   ├── ExternalAttachmentBanner.ts
│   │   │   ├── FieldConfigTab.css
│   │   │   ├── FormRenderer.ts
│   │   │   ├── FormRendererCss.ts
│   │   │   ├── Forms/
│   │   │   │   ├── Columns.ts
│   │   │   │   ├── Editor.ts
│   │   │   │   ├── Field.ts
│   │   │   │   ├── FormConfig.ts
│   │   │   │   ├── FormView.ts
│   │   │   │   ├── MappedFieldsConfig.ts
│   │   │   │   ├── Menu.ts
│   │   │   │   ├── Model.ts
│   │   │   │   ├── Paragraph.ts
│   │   │   │   ├── Section.ts
│   │   │   │   ├── Submit.ts
│   │   │   │   ├── elements.ts
│   │   │   │   └── styles.ts
│   │   │   ├── FormulaTransform.ts
│   │   │   ├── GridView.css
│   │   │   ├── GridView.ts
│   │   │   ├── GristClientSocket.ts
│   │   │   ├── GristDoc.css
│   │   │   ├── GristDoc.ts
│   │   │   ├── GristWSConnection.ts
│   │   │   ├── Importer.ts
│   │   │   ├── KeyboardFocusHighlighter.ts
│   │   │   ├── Layout.css
│   │   │   ├── Layout.ts
│   │   │   ├── LayoutEditor.css
│   │   │   ├── LayoutEditor.ts
│   │   │   ├── LayoutTray.ts
│   │   │   ├── LinkingState.ts
│   │   │   ├── Login.css
│   │   │   ├── ParseOptions.ts
│   │   │   ├── PluginScreen.ts
│   │   │   ├── Printing.css
│   │   │   ├── Printing.ts
│   │   │   ├── RawDataPage.ts
│   │   │   ├── RecordCardPopup.ts
│   │   │   ├── RecordLayout.css
│   │   │   ├── RecordLayout.js
│   │   │   ├── RecordLayoutEditor.js
│   │   │   ├── RefSelect.ts
│   │   │   ├── RegionFocusSwitcher.ts
│   │   │   ├── SearchBar.css
│   │   │   ├── SelectionSummary.ts
│   │   │   ├── TypeConversion.ts
│   │   │   ├── TypeTransform.ts
│   │   │   ├── UndoStack.ts
│   │   │   ├── UnsavedChanges.ts
│   │   │   ├── VersionUpdateBanner.ts
│   │   │   ├── ViewAsBanner.ts
│   │   │   ├── ViewConfigTab.css
│   │   │   ├── ViewConfigTab.js
│   │   │   ├── ViewLayout.css
│   │   │   ├── ViewLayout.ts
│   │   │   ├── ViewLinker.css
│   │   │   ├── ViewPane.ts
│   │   │   ├── VirtualDoc.ts
│   │   │   ├── VirtualTable.ts
│   │   │   ├── WidgetFrame.ts
│   │   │   ├── buildViewSectionDom.ts
│   │   │   ├── commandList.ts
│   │   │   ├── commands.css
│   │   │   ├── commands.ts
│   │   │   ├── duplicatePage.ts
│   │   │   ├── duplicateWidget.ts
│   │   │   ├── modals.ts
│   │   │   ├── viewCommon.css
│   │   │   └── viewCommon.js
│   │   ├── declarations.d.ts
│   │   ├── errorMain.ts
│   │   ├── exposeModulesForTests.js
│   │   ├── formMain.ts
│   │   ├── lib/
│   │   │   ├── ACIndex.ts
│   │   │   ├── ACSelect.ts
│   │   │   ├── ACUserManager.ts
│   │   │   ├── BoxSpec.ts
│   │   │   ├── CellDiffTool.ts
│   │   │   ├── CustomSectionElement.ts
│   │   │   ├── Delay.ts
│   │   │   ├── DocPluginManager.ts
│   │   │   ├── DocSchemaImport.ts
│   │   │   ├── FocusLayer.ts
│   │   │   ├── GristWindow.ts
│   │   │   ├── HomePluginManager.ts
│   │   │   ├── ImportSourceElement.ts
│   │   │   ├── Mousetrap.js
│   │   │   ├── MultiUserManager.ts
│   │   │   ├── ObservableMap.js
│   │   │   ├── ObservableSet.js
│   │   │   ├── ReferenceUtils.ts
│   │   │   ├── SafeBrowser.ts
│   │   │   ├── SafeBrowserProcess.css
│   │   │   ├── Signal.ts
│   │   │   ├── Suggestions.ts
│   │   │   ├── TokenField.ts
│   │   │   ├── UrlState.ts
│   │   │   ├── Validator.ts
│   │   │   ├── airtable/
│   │   │   │   ├── AirtableImportUI.ts
│   │   │   │   ├── AirtableImporter.ts
│   │   │   │   ├── startDocAirtableImport.ts
│   │   │   │   └── startHomeAirtableImport.ts
│   │   │   ├── autocomplete.ts
│   │   │   ├── browserGlobals.ts
│   │   │   ├── browserInfo.ts
│   │   │   ├── chartUtil.ts
│   │   │   ├── clipboardUtils.ts
│   │   │   ├── dblclick.ts
│   │   │   ├── dispose.d.ts
│   │   │   ├── dispose.js
│   │   │   ├── dom.js
│   │   │   ├── domAsync.ts
│   │   │   ├── domUtils.ts
│   │   │   ├── download.js
│   │   │   ├── formUtils.ts
│   │   │   ├── formatUtils.ts
│   │   │   ├── fromKoSave.ts
│   │   │   ├── getOrCreateStyleElement.ts
│   │   │   ├── guessTimezone.ts
│   │   │   ├── hashUtils.ts
│   │   │   ├── helpScout.ts
│   │   │   ├── imports.d.ts
│   │   │   ├── imports.js
│   │   │   ├── isFocusable.ts
│   │   │   ├── koArray.d.ts
│   │   │   ├── koArray.js
│   │   │   ├── koArrayWrap.ts
│   │   │   ├── koDom.js
│   │   │   ├── koDomScrolly.css
│   │   │   ├── koDomScrolly.js
│   │   │   ├── koForm.css
│   │   │   ├── koForm.js
│   │   │   ├── koUtil.js
│   │   │   ├── loadScript.ts
│   │   │   ├── localStorageObs.ts
│   │   │   ├── localization.ts
│   │   │   ├── log.ts
│   │   │   ├── markdown.ts
│   │   │   ├── nameUtils.ts
│   │   │   ├── pausableObs.ts
│   │   │   ├── popupControl.ts
│   │   │   ├── popupUtils.ts
│   │   │   ├── sanitizeUrl.ts
│   │   │   ├── sessionObs.ts
│   │   │   ├── simpleList.ts
│   │   │   ├── sortUtil.ts
│   │   │   ├── storage.ts
│   │   │   ├── tableUtil.ts
│   │   │   ├── telemetry.ts
│   │   │   ├── testState.ts
│   │   │   ├── textUtils.ts
│   │   │   ├── timeUtils.ts
│   │   │   ├── trapTabKey.ts
│   │   │   ├── uploads.ts
│   │   │   └── urlUtils.ts
│   │   ├── logo.css
│   │   ├── models/
│   │   │   ├── AdminChecks.ts
│   │   │   ├── AppModel.ts
│   │   │   ├── AuditLogsModel.ts
│   │   │   ├── BaseRowModel.js
│   │   │   ├── ChatHistory.ts
│   │   │   ├── ClientColumnGetters.ts
│   │   │   ├── ColumnACIndexes.ts
│   │   │   ├── ColumnCache.ts
│   │   │   ├── ColumnFilter.ts
│   │   │   ├── ColumnFilterMenuModel.ts
│   │   │   ├── ColumnToMap.ts
│   │   │   ├── ConnectState.ts
│   │   │   ├── DataRowModel.ts
│   │   │   ├── DataTableModel.js
│   │   │   ├── DataTableModelWithDiff.ts
│   │   │   ├── DocData.ts
│   │   │   ├── DocModel.ts
│   │   │   ├── DocPageModel.ts
│   │   │   ├── FormModel.ts
│   │   │   ├── HomeModel.ts
│   │   │   ├── MetaRowModel.js
│   │   │   ├── MetaTableModel.js
│   │   │   ├── NotifyModel.ts
│   │   │   ├── QuerySet.ts
│   │   │   ├── RuleOwner.ts
│   │   │   ├── SearchModel.ts
│   │   │   ├── SectionFilter.ts
│   │   │   ├── Styles.ts
│   │   │   ├── TableData.ts
│   │   │   ├── TableModel.js
│   │   │   ├── TelemetryModel.ts
│   │   │   ├── TimeQuery.ts
│   │   │   ├── ToggleEnterpriseModel.ts
│   │   │   ├── TreeModel.ts
│   │   │   ├── UnionRowSource.ts
│   │   │   ├── UserManagerModel.ts
│   │   │   ├── UserPrefs.ts
│   │   │   ├── UserPresenceModel.ts
│   │   │   ├── ViewFieldConfig.ts
│   │   │   ├── VirtualTable.ts
│   │   │   ├── VirtualTableMeta.ts
│   │   │   ├── WorkspaceInfo.ts
│   │   │   ├── entities/
│   │   │   │   ├── ACLRuleRec.ts
│   │   │   │   ├── CellRec.ts
│   │   │   │   ├── ColumnRec.ts
│   │   │   │   ├── DocInfoRec.ts
│   │   │   │   ├── FilterRec.ts
│   │   │   │   ├── PageRec.ts
│   │   │   │   ├── ShareRec.ts
│   │   │   │   ├── TabBarRec.ts
│   │   │   │   ├── TableRec.ts
│   │   │   │   ├── ValidationRec.ts
│   │   │   │   ├── ViewFieldRec.ts
│   │   │   │   ├── ViewRec.ts
│   │   │   │   └── ViewSectionRec.ts
│   │   │   ├── errors.ts
│   │   │   ├── features.ts
│   │   │   ├── gristConfigCache.ts
│   │   │   ├── gristUrlState.ts
│   │   │   ├── homeUrl.ts
│   │   │   ├── modelUtil.js
│   │   │   ├── rowset.ts
│   │   │   └── rowuid.js
│   │   ├── tsconfig.json
│   │   ├── ui/
│   │   │   ├── AccountPage.ts
│   │   │   ├── AccountPageCss.ts
│   │   │   ├── AccountWidget.ts
│   │   │   ├── AccountWidgetCss.ts
│   │   │   ├── ActiveUserList.ts
│   │   │   ├── AddNewButton.ts
│   │   │   ├── AddNewTip.ts
│   │   │   ├── AdminLeftPanel.ts
│   │   │   ├── AdminPanel.ts
│   │   │   ├── AdminPanelCss.ts
│   │   │   ├── AdminPanelName.ts
│   │   │   ├── AdminTogglesCss.ts
│   │   │   ├── ApiKey.ts
│   │   │   ├── App.css
│   │   │   ├── App.ts
│   │   │   ├── AppHeader.ts
│   │   │   ├── AppUI.ts
│   │   │   ├── AuditLogStreamingConfig.ts
│   │   │   ├── AuditLogsPage.ts
│   │   │   ├── AuthenticationSection.ts
│   │   │   ├── BottomBar.ts
│   │   │   ├── CardContextMenu.ts
│   │   │   ├── CellContextMenu.ts
│   │   │   ├── ChangeAdminModal.ts
│   │   │   ├── CodeHighlight.ts
│   │   │   ├── ColumnFilterCalendarView.ts
│   │   │   ├── ColumnFilterMenu.ts
│   │   │   ├── ColumnFilterMenuUtils.ts
│   │   │   ├── ColumnTitle.ts
│   │   │   ├── ConfigsAPI.ts
│   │   │   ├── CoreHomeImports.ts
│   │   │   ├── CoreNewDocMethods.ts
│   │   │   ├── CreateTeamModal.ts
│   │   │   ├── CustomSectionConfig.ts
│   │   │   ├── CustomThemes.ts
│   │   │   ├── CustomWidgetGallery.ts
│   │   │   ├── DateRangeOptions.ts
│   │   │   ├── DefaultActivationPage.ts
│   │   │   ├── DescriptionConfig.ts
│   │   │   ├── DocHistory.ts
│   │   │   ├── DocIcon.ts
│   │   │   ├── DocList.ts
│   │   │   ├── DocMenu.ts
│   │   │   ├── DocMenuCss.ts
│   │   │   ├── DocTour.ts
│   │   │   ├── DocTutorial.css
│   │   │   ├── DocTutorial.ts
│   │   │   ├── DocTutorialRenderer.ts
│   │   │   ├── DocumentSettings.ts
│   │   │   ├── DuplicateTable.ts
│   │   │   ├── EmojiPicker.ts
│   │   │   ├── ExampleCard.ts
│   │   │   ├── ExampleInfo.ts
│   │   │   ├── Experiments.ts
│   │   │   ├── FieldConfig.ts
│   │   │   ├── FieldContextMenu.ts
│   │   │   ├── FieldMenus.ts
│   │   │   ├── FileDialog.ts
│   │   │   ├── FilterBar.ts
│   │   │   ├── FilterConfig.ts
│   │   │   ├── FloatingPopup.ts
│   │   │   ├── FormAPI.ts
│   │   │   ├── FormContainer.ts
│   │   │   ├── FormErrorPage.ts
│   │   │   ├── FormPage.ts
│   │   │   ├── FormSuccessPage.ts
│   │   │   ├── GetGristComProvider.ts
│   │   │   ├── GridOptions.ts
│   │   │   ├── GridViewMenus.ts
│   │   │   ├── GridViewMenusDateHelpers.ts
│   │   │   ├── GristTooltips.ts
│   │   │   ├── HomeIntro.ts
│   │   │   ├── HomeIntroCards.ts
│   │   │   ├── HomeLeftPane.ts
│   │   │   ├── IAssistantPopup.ts
│   │   │   ├── ImportProgress.ts
│   │   │   ├── LanguageMenu.ts
│   │   │   ├── LeftPanelCommon.ts
│   │   │   ├── LinkConfig.ts
│   │   │   ├── LoginPagesCss.ts
│   │   │   ├── MakeCopyMenu.ts
│   │   │   ├── MarkdownCellRenderer.ts
│   │   │   ├── MenuToggle.ts
│   │   │   ├── MultiSelector.ts
│   │   │   ├── NewRecordButton.ts
│   │   │   ├── NotifyUI.ts
│   │   │   ├── OnBoardingPopups.ts
│   │   │   ├── OnboardingPage.ts
│   │   │   ├── OpenAccessibilityModal.ts
│   │   │   ├── OpenUserManager.ts
│   │   │   ├── OpenVideoTour.ts
│   │   │   ├── PagePanels.ts
│   │   │   ├── PageWidgetPicker.ts
│   │   │   ├── Pages.ts
│   │   │   ├── PinnedDocs.ts
│   │   │   ├── PredefinedCustomSectionConfig.ts
│   │   │   ├── ProposedChangesPage.ts
│   │   │   ├── RelativeDatesOptions.ts
│   │   │   ├── RenameDocModal.ts
│   │   │   ├── RenamePopupStyles.ts
│   │   │   ├── RightPanel.ts
│   │   │   ├── RightPanelStyles.ts
│   │   │   ├── RightPanelUtils.ts
│   │   │   ├── RowContextMenu.ts
│   │   │   ├── RowHeightConfig.ts
│   │   │   ├── ShareMenu.ts
│   │   │   ├── ShortcutKey.ts
│   │   │   ├── SiteSwitcher.ts
│   │   │   ├── SortConfig.ts
│   │   │   ├── SortFilterConfig.ts
│   │   │   ├── SupportGristButton.ts
│   │   │   ├── SupportGristPage.ts
│   │   │   ├── TemplateDocs.ts
│   │   │   ├── ThemeConfig.ts
│   │   │   ├── TimingPage.ts
│   │   │   ├── ToggleEnterpriseWidget.ts
│   │   │   ├── Tools.ts
│   │   │   ├── TopBar.ts
│   │   │   ├── TopBarCss.ts
│   │   │   ├── TreeViewComponent.ts
│   │   │   ├── TreeViewComponentCss.ts
│   │   │   ├── TriggerFormulas.ts
│   │   │   ├── UserImage.ts
│   │   │   ├── UserItem.ts
│   │   │   ├── UserManager.ts
│   │   │   ├── ViewLayoutMenu.ts
│   │   │   ├── ViewSectionMenu.ts
│   │   │   ├── VisibleFieldsConfig.ts
│   │   │   ├── WebhookPage.ts
│   │   │   ├── WelcomeCoachingCall.ts
│   │   │   ├── WelcomePage.ts
│   │   │   ├── WelcomeSitePicker.ts
│   │   │   ├── WelcomeTour.ts
│   │   │   ├── WidgetTitle.ts
│   │   │   ├── YouTubePlayer.ts
│   │   │   ├── buildReassignModal.ts
│   │   │   ├── buttons.ts
│   │   │   ├── contextMenu.ts
│   │   │   ├── createAppPage.ts
│   │   │   ├── createPage.ts
│   │   │   ├── cssInput.ts
│   │   │   ├── errorPages.ts
│   │   │   ├── forms.ts
│   │   │   ├── googleAuth.ts
│   │   │   ├── inputs.ts
│   │   │   ├── mouseDrag.ts
│   │   │   ├── resizeHandle.ts
│   │   │   ├── sanitizeHTML.ts
│   │   │   ├── searchDropdown.ts
│   │   │   ├── selectBy.ts
│   │   │   ├── sendToDrive.ts
│   │   │   ├── shadowScroll.ts
│   │   │   ├── tooltips.ts
│   │   │   ├── transientInput.ts
│   │   │   ├── transitions.ts
│   │   │   ├── userTrustsCustomWidget.ts
│   │   │   ├── viewport.ts
│   │   │   └── widgetTypesMap.ts
│   │   ├── ui2018/
│   │   │   ├── ColorPalette.ts
│   │   │   ├── ColorSelect.ts
│   │   │   ├── IconList.ts
│   │   │   ├── alerts.ts
│   │   │   ├── ariaTabs.ts
│   │   │   ├── breadcrumbs.ts
│   │   │   ├── buttonSelect.ts
│   │   │   ├── buttons.ts
│   │   │   ├── checkbox.ts
│   │   │   ├── cssVars.ts
│   │   │   ├── draggableList.ts
│   │   │   ├── editableLabel.ts
│   │   │   ├── icons.ts
│   │   │   ├── links.ts
│   │   │   ├── loaders.ts
│   │   │   ├── menus.ts
│   │   │   ├── modals.ts
│   │   │   ├── pages.ts
│   │   │   ├── popups.ts
│   │   │   ├── radio.ts
│   │   │   ├── search.ts
│   │   │   ├── select.ts
│   │   │   ├── stretchedLink.ts
│   │   │   ├── tabs.ts
│   │   │   ├── theme.ts
│   │   │   ├── toggleSwitch.ts
│   │   │   ├── unstyled.ts
│   │   │   └── visuallyHidden.ts
│   │   └── widgets/
│   │       ├── AbstractWidget.js
│   │       ├── Assistant.ts
│   │       ├── AttachmentsEditor.ts
│   │       ├── AttachmentsWidget.ts
│   │       ├── BaseEditor.js
│   │       ├── CellStyle.ts
│   │       ├── CheckBox.css
│   │       ├── CheckBoxEditor.js
│   │       ├── ChoiceEditor.js
│   │       ├── ChoiceListCell.ts
│   │       ├── ChoiceListEditor.ts
│   │       ├── ChoiceListEntry.ts
│   │       ├── ChoiceTextBox.ts
│   │       ├── ChoiceToken.ts
│   │       ├── ConditionalStyle.ts
│   │       ├── CurrencyPicker.ts
│   │       ├── DateEditor.ts
│   │       ├── DateTextBox.js
│   │       ├── DateTimeEditor.css
│   │       ├── DateTimeEditor.ts
│   │       ├── DateTimeTextBox.js
│   │       ├── DiffBox.ts
│   │       ├── DiscussionEditor.ts
│   │       ├── EditorButtons.ts
│   │       ├── EditorPlacement.ts
│   │       ├── EditorTooltip.ts
│   │       ├── ErrorDom.ts
│   │       ├── FieldBuilder.css
│   │       ├── FieldBuilder.ts
│   │       ├── FieldEditor.ts
│   │       ├── FloatingEditor.ts
│   │       ├── FormulaAssistant.ts
│   │       ├── FormulaEditor.ts
│   │       ├── HyperLinkEditor.ts
│   │       ├── HyperLinkTextBox.ts
│   │       ├── MarkdownTextBox.ts
│   │       ├── MentionTextBox.ts
│   │       ├── NTextBox.ts
│   │       ├── NTextEditor.ts
│   │       ├── NewAbstractWidget.ts
│   │       ├── NewBaseEditor.ts
│   │       ├── NumericEditor.ts
│   │       ├── NumericSpinner.ts
│   │       ├── NumericTextBox.ts
│   │       ├── Reference.css
│   │       ├── Reference.ts
│   │       ├── ReferenceEditor.ts
│   │       ├── ReferenceList.ts
│   │       ├── ReferenceListEditor.ts
│   │       ├── ReverseReferenceConfig.ts
│   │       ├── Spinner.css
│   │       ├── Spinner.ts
│   │       ├── TZAutocomplete.ts
│   │       ├── TextBox.css
│   │       ├── TextEditor.css
│   │       ├── TextEditor.js
│   │       ├── Toggle.ts
│   │       ├── UserType.ts
│   │       └── UserTypeImpl.ts
│   ├── common/
│   │   ├── ACLPermissions.ts
│   │   ├── ACLRuleCollection.ts
│   │   ├── ACLRulesReader.ts
│   │   ├── ActionBundle.ts
│   │   ├── ActionDispatcher.ts
│   │   ├── ActionGroup.ts
│   │   ├── ActionRouter.ts
│   │   ├── ActionSummarizer.ts
│   │   ├── ActionSummary.ts
│   │   ├── ActivationAPI.ts
│   │   ├── ActiveDocAPI.ts
│   │   ├── AlternateActions.ts
│   │   ├── ApiError.ts
│   │   ├── Assistance.ts
│   │   ├── Assistant.ts
│   │   ├── AsyncCreate.ts
│   │   ├── AsyncFlow.ts
│   │   ├── AttachmentColumns.ts
│   │   ├── BaseAPI.ts
│   │   ├── BasketClientAPI.ts
│   │   ├── BigInt.ts
│   │   ├── BillingAPI.ts
│   │   ├── BinaryIndexedTree.js
│   │   ├── BootProbe.ts
│   │   ├── BrowserSettings.ts
│   │   ├── CircularArray.js
│   │   ├── ColumnFilterFunc.ts
│   │   ├── ColumnGetters.ts
│   │   ├── CommTypes.ts
│   │   ├── Config-ti.ts
│   │   ├── Config.ts
│   │   ├── ConfigAPI.ts
│   │   ├── CssCustomProp.ts
│   │   ├── CustomWidget.ts
│   │   ├── DisposableWithEvents.ts
│   │   ├── DocActions.ts
│   │   ├── DocComments.ts
│   │   ├── DocData.ts
│   │   ├── DocDataCache.ts
│   │   ├── DocLimits.ts
│   │   ├── DocListAPI.ts
│   │   ├── DocSchemaImport.ts
│   │   ├── DocSchemaImportTypes-ti.ts
│   │   ├── DocSchemaImportTypes.ts
│   │   ├── DocSnapshot.ts
│   │   ├── DocState.ts
│   │   ├── DocUsage.ts
│   │   ├── DocumentSettings-ti.ts
│   │   ├── DocumentSettings.ts
│   │   ├── DropdownCondition.ts
│   │   ├── EncActionBundle.ts
│   │   ├── ErrorWithCode.ts
│   │   ├── Features-ti.ts
│   │   ├── Features.ts
│   │   ├── FilterState.ts
│   │   ├── Forms.ts
│   │   ├── Formula.ts
│   │   ├── GranularAccessClause.ts
│   │   ├── GristServerAPI.ts
│   │   ├── ICommonUrls-ti.ts
│   │   ├── ICommonUrls.ts
│   │   ├── InactivityTimer.ts
│   │   ├── Install.ts
│   │   ├── InstallAPI.ts
│   │   ├── Interval.ts
│   │   ├── KeyedMutex.ts
│   │   ├── KeyedOps.ts
│   │   ├── Limits.ts
│   │   ├── LinkNode.ts
│   │   ├── LocaleCodes.ts
│   │   ├── Locales.ts
│   │   ├── LoginSessionAPI.ts
│   │   ├── MemBuffer.js
│   │   ├── NumberFormat.ts
│   │   ├── NumberParse.ts
│   │   ├── PluginInstance.ts
│   │   ├── PredicateFormula.ts
│   │   ├── Prefs.ts
│   │   ├── RecentItems.js
│   │   ├── RecordView.ts
│   │   ├── RefCountMap.ts
│   │   ├── RelativeDates.ts
│   │   ├── RowFilterFunc.ts
│   │   ├── SandboxInfo.ts
│   │   ├── ServiceAccountTypes-ti.ts
│   │   ├── ServiceAccountTypes.ts
│   │   ├── ShareAnnotator.ts
│   │   ├── ShareOptions.ts
│   │   ├── SortFunc.ts
│   │   ├── SortSpec.ts
│   │   ├── StringUnion.ts
│   │   ├── TableData.ts
│   │   ├── TabularDiff.ts
│   │   ├── Telemetry.ts
│   │   ├── TestState.ts
│   │   ├── ThemePrefs.ts
│   │   ├── Themes.ts
│   │   ├── TimeQuery.ts
│   │   ├── Triggers-ti.ts
│   │   ├── Triggers.ts
│   │   ├── User.ts
│   │   ├── UserAPI.ts
│   │   ├── UserConfig.ts
│   │   ├── ValueConverter.ts
│   │   ├── ValueFormatter.ts
│   │   ├── ValueGuesser.ts
│   │   ├── ValueParser.ts
│   │   ├── WidgetOptions.ts
│   │   ├── airtable/
│   │   │   ├── AirtableAPI.ts
│   │   │   ├── AirtableAPITypes-ti.ts
│   │   │   ├── AirtableAPITypes.ts
│   │   │   ├── AirtableAttachmentTracker.ts
│   │   │   ├── AirtableCrosswalk.ts
│   │   │   ├── AirtableDataImporter.ts
│   │   │   ├── AirtableDataImporterTypes.ts
│   │   │   ├── AirtableReferenceTracker.ts
│   │   │   └── AirtableSchemaImporter.ts
│   │   ├── arrayToString.ts
│   │   ├── asyncIterators.ts
│   │   ├── csvFormat.ts
│   │   ├── declarations.d.ts
│   │   ├── delay.ts
│   │   ├── emails.ts
│   │   ├── getCurrentTime.ts
│   │   ├── gristTypes.ts
│   │   ├── gristUrls.ts
│   │   ├── gutil.ts
│   │   ├── isHiddenTable.ts
│   │   ├── loginProviders.ts
│   │   ├── marshal.ts
│   │   ├── normalizedDateTimeString.ts
│   │   ├── orgNameUtils.ts
│   │   ├── parseDate.ts
│   │   ├── plugin.ts
│   │   ├── resetOrg.ts
│   │   ├── roles.ts
│   │   ├── schema.ts
│   │   ├── tagManager.ts
│   │   ├── tbind.ts
│   │   ├── themes/
│   │   │   ├── Base.ts
│   │   │   ├── GristDark.ts
│   │   │   ├── GristLight.ts
│   │   │   └── HighContrastLight.ts
│   │   ├── timeFormat.ts
│   │   ├── tpromisified.ts
│   │   ├── tsconfig.json
│   │   ├── tsvFormat.ts
│   │   ├── uploads.ts
│   │   ├── urlUtils.ts
│   │   └── widgetTypes.ts
│   ├── gen-server/
│   │   ├── ApiServer.ts
│   │   ├── entity/
│   │   │   ├── AclRule.ts
│   │   │   ├── Activation.ts
│   │   │   ├── Alias.ts
│   │   │   ├── BillingAccount.ts
│   │   │   ├── BillingAccountManager.ts
│   │   │   ├── Config.ts
│   │   │   ├── DocPref.ts
│   │   │   ├── Document.ts
│   │   │   ├── Group.ts
│   │   │   ├── Limit.ts
│   │   │   ├── Login.ts
│   │   │   ├── OAuthClient.ts
│   │   │   ├── OAuthGrant.ts
│   │   │   ├── Organization.ts
│   │   │   ├── Pref.ts
│   │   │   ├── Product.ts
│   │   │   ├── Proposal.ts
│   │   │   ├── Resource.ts
│   │   │   ├── Secret.ts
│   │   │   ├── ServiceAccount.ts
│   │   │   ├── Share.ts
│   │   │   ├── User.ts
│   │   │   └── Workspace.ts
│   │   ├── lib/
│   │   │   ├── ActivationsManager.ts
│   │   │   ├── DocApiForwarder.ts
│   │   │   ├── DocWorkerMap.ts
│   │   │   ├── Doom.ts
│   │   │   ├── Housekeeper.ts
│   │   │   ├── NotifierTypes.ts
│   │   │   ├── Permissions.ts
│   │   │   ├── TypeORMPatches.ts
│   │   │   ├── Usage.ts
│   │   │   ├── homedb/
│   │   │   │   ├── Caches.ts
│   │   │   │   ├── GroupsManager.ts
│   │   │   │   ├── HomeDBManager.ts
│   │   │   │   ├── Interfaces.ts
│   │   │   │   ├── ServiceAccountsManager.ts
│   │   │   │   └── UsersManager.ts
│   │   │   ├── scrubUserFromOrg.ts
│   │   │   └── values.ts
│   │   ├── migration/
│   │   │   ├── 1536634251710-Initial.ts
│   │   │   ├── 1539031763952-Login.ts
│   │   │   ├── 1549313797109-PinDocs.ts
│   │   │   ├── 1549381727494-UserPicture.ts
│   │   │   ├── 1551805156919-LoginDisplayEmail.ts
│   │   │   ├── 1552416614755-LoginDisplayEmailNonNull.ts
│   │   │   ├── 1553016106336-Indexes.ts
│   │   │   ├── 1556726945436-Billing.ts
│   │   │   ├── 1557157922339-OrgDomainUnique.ts
│   │   │   ├── 1561589211752-Aliases.ts
│   │   │   ├── 1568238234987-TeamMembers.ts
│   │   │   ├── 1569593726320-FirstLogin.ts
│   │   │   ├── 1569946508569-FirstTimeUser.ts
│   │   │   ├── 1573569442552-CustomerIndex.ts
│   │   │   ├── 1579559983067-ExtraIndexes.ts
│   │   │   ├── 1591755411755-OrgHost.ts
│   │   │   ├── 1592261300044-DocRemovedAt.ts
│   │   │   ├── 1596456522124-Prefs.ts
│   │   │   ├── 1623871765992-ExternalBilling.ts
│   │   │   ├── 1626369037484-DocOptions.ts
│   │   │   ├── 1631286208009-Secret.ts
│   │   │   ├── 1644363380225-UserOptions.ts
│   │   │   ├── 1647883793388-GracePeriodStart.ts
│   │   │   ├── 1651469582887-DocumentUsage.ts
│   │   │   ├── 1652273656610-Activations.ts
│   │   │   ├── 1652277549983-UserConnectId.ts
│   │   │   ├── 1663851423064-UserUUID.ts
│   │   │   ├── 1664528376930-UserRefUnique.ts
│   │   │   ├── 1673051005072-Forks.ts
│   │   │   ├── 1678737195050-ForkIndexes.ts
│   │   │   ├── 1682636695021-ActivationPrefs.ts
│   │   │   ├── 1685343047786-AssistantLimit.ts
│   │   │   ├── 1701557445716-Shares.ts
│   │   │   ├── 1711557445716-Billing.ts
│   │   │   ├── 1713186031023-UserLastConnection.ts
│   │   │   ├── 1722529827161-Activation-Enabled.ts
│   │   │   ├── 1727747249153-Configs.ts
│   │   │   ├── 1729754662550-LoginsEmailIndex.ts
│   │   │   ├── 1732103776245-GracePeriod.ts
│   │   │   ├── 1738912357827-UserCreatedAt.ts
│   │   │   ├── 1746246433628-DocPref.ts
│   │   │   ├── 1749454162428-GroupUsersCreatedAt.ts
│   │   │   ├── 1753088213255-GroupTypes.ts
│   │   │   ├── 1754077317821-UserDisabledAt.ts
│   │   │   ├── 1756799894986-UserUnsubscribeKey.ts
│   │   │   ├── 1756918816559-ServiceAccounts.ts
│   │   │   ├── 1759256005608-Proposals.ts
│   │   │   ├── 1759434763338-DocDisabledAt.ts
│   │   │   ├── 1764872085347-OAuthClientsAndGrants.ts
│   │   │   └── README.md
│   │   └── sqlUtils.ts
│   ├── plugin/
│   │   ├── CustomSectionAPI-ti.ts
│   │   ├── CustomSectionAPI.ts
│   │   ├── DocApiTypes-ti.ts
│   │   ├── DocApiTypes.ts
│   │   ├── FileParserAPI-ti.ts
│   │   ├── FileParserAPI.ts
│   │   ├── GristAPI-ti.ts
│   │   ├── GristAPI.ts
│   │   ├── GristData-ti.ts
│   │   ├── GristData.ts
│   │   ├── GristTable-ti.ts
│   │   ├── GristTable.ts
│   │   ├── ImportSourceAPI-ti.ts
│   │   ├── ImportSourceAPI.ts
│   │   ├── InternalImportSourceAPI-ti.ts
│   │   ├── InternalImportSourceAPI.ts
│   │   ├── PluginManifest-ti.ts
│   │   ├── PluginManifest.ts
│   │   ├── README.md
│   │   ├── RenderOptions-ti.ts
│   │   ├── RenderOptions.ts
│   │   ├── StorageAPI-ti.ts
│   │   ├── StorageAPI.ts
│   │   ├── TableOperations.ts
│   │   ├── TableOperationsImpl.ts
│   │   ├── TypeCheckers.ts
│   │   ├── WidgetAPI-ti.ts
│   │   ├── WidgetAPI.ts
│   │   ├── grist-plugin-api.ts
│   │   ├── gutil.ts
│   │   ├── objtypes.ts
│   │   └── tsconfig.json
│   ├── server/
│   │   ├── MergedServer.ts
│   │   ├── companion.ts
│   │   ├── declarations.d.ts
│   │   ├── devServerMain.ts
│   │   ├── generateCheckpoint.ts
│   │   ├── generateInitialDocSql.ts
│   │   ├── lib/
│   │   │   ├── AccessTokens.ts
│   │   │   ├── ActionHistory.ts
│   │   │   ├── ActionHistoryImpl.ts
│   │   │   ├── ActiveDoc.ts
│   │   │   ├── ActiveDocImport.ts
│   │   │   ├── ActiveDocUtils.ts
│   │   │   ├── AppEndpoint.ts
│   │   │   ├── AppSettings.ts
│   │   │   ├── Archive.ts
│   │   │   ├── Assistant.ts
│   │   │   ├── AssistantStatePermit.ts
│   │   │   ├── AttachmentFileManager.ts
│   │   │   ├── AttachmentStore.ts
│   │   │   ├── AttachmentStoreProvider.ts
│   │   │   ├── AuditEvent.ts
│   │   │   ├── AuthSession.ts
│   │   │   ├── Authorizer.ts
│   │   │   ├── BootProbes.ts
│   │   │   ├── BrowserSession.ts
│   │   │   ├── CellDataAccess.ts
│   │   │   ├── Client.ts
│   │   │   ├── Comm.ts
│   │   │   ├── ConfigBackendAPI.ts
│   │   │   ├── DiscourseConnect.ts
│   │   │   ├── DocApi.ts
│   │   │   ├── DocApiTriggers.ts
│   │   │   ├── DocApiUtils.ts
│   │   │   ├── DocAuthorizer.ts
│   │   │   ├── DocClients.ts
│   │   │   ├── DocManager.ts
│   │   │   ├── DocPluginData.ts
│   │   │   ├── DocPluginManager.ts
│   │   │   ├── DocSession.ts
│   │   │   ├── DocSnapshots.ts
│   │   │   ├── DocStorage.ts
│   │   │   ├── DocStorageManager.ts
│   │   │   ├── DocWorker.ts
│   │   │   ├── DocWorkerLoadTracker.ts
│   │   │   ├── DocWorkerMap.ts
│   │   │   ├── DocWorkerUtils.ts
│   │   │   ├── ExcelFormatter.ts
│   │   │   ├── ExpandedQuery.ts
│   │   │   ├── Export.ts
│   │   │   ├── ExportDSV.ts
│   │   │   ├── ExportTableSchema.ts
│   │   │   ├── ExportXLSX.ts
│   │   │   ├── ExternalStorage.ts
│   │   │   ├── FileParserElement.ts
│   │   │   ├── FlexServer.ts
│   │   │   ├── ForwardAuthLogin.ts
│   │   │   ├── GetGristComConfig.ts
│   │   │   ├── GoogleAuth.ts
│   │   │   ├── GoogleExport.ts
│   │   │   ├── GoogleImport.ts
│   │   │   ├── GranularAccess.ts
│   │   │   ├── GristJobs.ts
│   │   │   ├── GristServer.ts
│   │   │   ├── GristServerSocket.ts
│   │   │   ├── GristSocketServer.ts
│   │   │   ├── HashUtil.ts
│   │   │   ├── HostedMetadataManager.ts
│   │   │   ├── HostedStorageManager.ts
│   │   │   ├── IAssistant.ts
│   │   │   ├── IAuditLogger.ts
│   │   │   ├── IBilling.ts
│   │   │   ├── IChecksumStore.ts
│   │   │   ├── ICreate.ts
│   │   │   ├── IDocNotificationManager.ts
│   │   │   ├── IDocStorageManager.ts
│   │   │   ├── IElectionStore.ts
│   │   │   ├── INotifier.ts
│   │   │   ├── ISandbox.ts
│   │   │   ├── IShell.ts
│   │   │   ├── ITestingHooks-ti.ts
│   │   │   ├── ITestingHooks.ts
│   │   │   ├── InsightLog.ts
│   │   │   ├── InstallAdmin.ts
│   │   │   ├── LogMethods.ts
│   │   │   ├── LoginSystemConfig.ts
│   │   │   ├── MemoryPool.ts
│   │   │   ├── MinIOExternalStorage.ts
│   │   │   ├── MinimalLogin.ts
│   │   │   ├── NSandbox.ts
│   │   │   ├── NullSandbox.ts
│   │   │   ├── OAuth2Clients.ts
│   │   │   ├── OIDCConfig.ts
│   │   │   ├── OnDemandActions.ts
│   │   │   ├── OpenAIAssistantV1.ts
│   │   │   ├── Patch.ts
│   │   │   ├── PermissionInfo.ts
│   │   │   ├── Permit.ts
│   │   │   ├── PluginEndpoint.ts
│   │   │   ├── PluginManager.ts
│   │   │   ├── ProcessMonitor.ts
│   │   │   ├── ProxyAgent.ts
│   │   │   ├── PubSubCache.ts
│   │   │   ├── PubSubManager.ts
│   │   │   ├── Requests.ts
│   │   │   ├── RowAccess.ts
│   │   │   ├── SQLiteDB.ts
│   │   │   ├── SafePythonComponent.ts
│   │   │   ├── SamlConfig.ts
│   │   │   ├── SandboxControl.ts
│   │   │   ├── SandboxPyodide.ts
│   │   │   ├── ServerColumnGetters.ts
│   │   │   ├── ServerLocale.ts
│   │   │   ├── Sessions.ts
│   │   │   ├── Sharing.ts
│   │   │   ├── SqliteCommon.ts
│   │   │   ├── SqliteNode.ts
│   │   │   ├── TableMetadataLoader.ts
│   │   │   ├── TagChecker.ts
│   │   │   ├── Telemetry.ts
│   │   │   ├── TestLogin.ts
│   │   │   ├── TestingHooks.ts
│   │   │   ├── Throttle.ts
│   │   │   ├── TimeQuery.ts
│   │   │   ├── Triggers.ts
│   │   │   ├── UnsafeNodeComponent.ts
│   │   │   ├── UpdateManager.ts
│   │   │   ├── UserPresence.ts
│   │   │   ├── WebhookQueue.ts
│   │   │   ├── WidgetRepository.ts
│   │   │   ├── attachEarlyEndpoints.ts
│   │   │   ├── backupSqliteDatabase.ts
│   │   │   ├── checksumFile.ts
│   │   │   ├── config.ts
│   │   │   ├── configCore.ts
│   │   │   ├── configCoreFileFormats-ti.ts
│   │   │   ├── configCoreFileFormats.ts
│   │   │   ├── configureMinIOExternalStorage.ts
│   │   │   ├── configureOpenAIAssistantV1.ts
│   │   │   ├── cookieUtils.ts
│   │   │   ├── coreCreator.ts
│   │   │   ├── coreLogins.ts
│   │   │   ├── createSavedDoc.ts
│   │   │   ├── dbUtils.ts
│   │   │   ├── describeDocActions.ts
│   │   │   ├── docUtils.d.ts
│   │   │   ├── docUtils.js
│   │   │   ├── expressWrap.ts
│   │   │   ├── extractOrg.ts
│   │   │   ├── filterUtils.ts
│   │   │   ├── gristSessions.ts
│   │   │   ├── gristSettings.ts
│   │   │   ├── guessExt.ts
│   │   │   ├── hashingUtils.ts
│   │   │   ├── httpEncoding.ts
│   │   │   ├── idUtils.ts
│   │   │   ├── initialDocSql.ts
│   │   │   ├── log.ts
│   │   │   ├── loginSystemHelpers.ts
│   │   │   ├── manifest.ts
│   │   │   ├── middleware.ts
│   │   │   ├── oidc/
│   │   │   │   └── Protections.ts
│   │   │   ├── places.ts
│   │   │   ├── reportTimeTaken.ts
│   │   │   ├── requestUtils.ts
│   │   │   ├── runSQLQuery.ts
│   │   │   ├── sandboxUtil.ts
│   │   │   ├── scim/
│   │   │   │   ├── index.ts
│   │   │   │   └── v2/
│   │   │   │       ├── BaseController.ts
│   │   │   │       ├── ScimGroupController.ts
│   │   │   │       ├── ScimRoleController.ts
│   │   │   │       ├── ScimTypes.ts
│   │   │   │       ├── ScimUserController.ts
│   │   │   │       ├── ScimUtils.ts
│   │   │   │       ├── ScimV2Api.ts
│   │   │   │       └── roles/
│   │   │   │           ├── SCIMMYRoleResource.ts
│   │   │   │           └── SCIMMYRoleSchema.ts
│   │   │   ├── selectBy.ts
│   │   │   ├── sendAppPage.ts
│   │   │   ├── serverUtils.ts
│   │   │   ├── sessionUtils.ts
│   │   │   ├── shortDesc.ts
│   │   │   ├── shutdown.js
│   │   │   ├── updateChecker.ts
│   │   │   ├── uploads.ts
│   │   │   └── workerExporter.ts
│   │   ├── localization.ts
│   │   ├── tsconfig.json
│   │   └── utils/
│   │       ├── LogSanitizer.ts
│   │       ├── gristify.ts
│   │       ├── pruneActionHistory.ts
│   │       ├── showAuditLogEvents.ts
│   │       └── streams.ts
│   └── tsconfig.json
├── buildtools/
│   ├── .grist-ee-version
│   ├── build.sh
│   ├── checkout-ext-directory.sh
│   ├── fly-deploy.js
│   ├── fly-template.env
│   ├── fly-template.toml
│   ├── genIconCSS.ts
│   ├── generate_locale_list.js
│   ├── generate_translation_keys.js
│   ├── install_chrome_for_tests.sh
│   ├── prepare_ee.sh
│   ├── prepare_python.sh
│   ├── sanitize_translations.js
│   ├── tsconfig-base-ext.json
│   ├── tsconfig-base.json
│   ├── update_schema.sh
│   ├── update_type_info.sh
│   ├── webpack.api.config.js
│   ├── webpack.check.js
│   └── webpack.config.js
├── crowdin.yml
├── docker-compose-examples/
│   ├── grist-local-testing/
│   │   ├── README.md
│   │   └── docker-compose.yml
│   ├── grist-traefik-basic-auth/
│   │   ├── README.md
│   │   ├── configs/
│   │   │   ├── traefik-config.yml
│   │   │   └── traefik-dynamic-config.yml
│   │   └── docker-compose.yml
│   ├── grist-traefik-oidc-auth/
│   │   ├── README.md
│   │   ├── configs/
│   │   │   ├── authelia/
│   │   │   │   ├── configuration.yml
│   │   │   │   └── users_database.yml
│   │   │   └── traefik/
│   │   │       └── config.yml
│   │   ├── docker-compose.yml
│   │   ├── env-template
│   │   ├── generateSecureSecrets.sh
│   │   └── secrets_template/
│   │       ├── GRIST_CLIENT_SECRET_DIGEST
│   │       ├── HMAC_SECRET
│   │       ├── JWT_SECRET
│   │       ├── SESSION_SECRET
│   │       ├── STORAGE_ENCRYPTION_KEY
│   │       └── certs/
│   │           └── private.pem
│   ├── grist-with-keycloak-postgres-redis-minio/
│   │   ├── README.md
│   │   └── docker-compose.yml
│   └── grist-with-postgres-redis-minio/
│       ├── README.md
│       └── docker-compose.yml
├── documentation/
│   ├── database.md
│   ├── develop.md
│   ├── disposal.md
│   ├── grainjs.md
│   ├── grist-data-format.md
│   ├── images/
│   │   └── BDD.drawio
│   ├── migrations.md
│   ├── overview.md
│   ├── translations.md
│   └── urls.md
├── eslint.config.js
├── package.json
├── plugins/
│   └── core/
│       └── manifest.yml
├── publiccode.yml
├── sandbox/
│   ├── MANIFEST.in
│   ├── bundle_as_wheel.sh
│   ├── docker/
│   │   ├── Dockerfile
│   │   └── Makefile
│   ├── docker_entrypoint.sh
│   ├── gen_js_schema.py
│   ├── grist/
│   │   ├── acl.py
│   │   ├── action_obj.py
│   │   ├── action_summary.py
│   │   ├── actions.py
│   │   ├── attribute_recorder.py
│   │   ├── autocomplete_context.py
│   │   ├── codebuilder.py
│   │   ├── column.py
│   │   ├── csv_patch.py
│   │   ├── depend.py
│   │   ├── docactions.py
│   │   ├── docmodel.py
│   │   ├── dropdown_condition.py
│   │   ├── engine.py
│   │   ├── fake_std_streams.py
│   │   ├── formula_prompt.py
│   │   ├── friendly_errors.py
│   │   ├── functions/
│   │   │   ├── __init__.py
│   │   │   ├── date.py
│   │   │   ├── info.py
│   │   │   ├── logical.py
│   │   │   ├── lookup.py
│   │   │   ├── math.py
│   │   │   ├── prevnext.py
│   │   │   ├── schedule.py
│   │   │   ├── stats.py
│   │   │   ├── test_schedule.py
│   │   │   ├── text.py
│   │   │   └── unimplemented.py
│   │   ├── gencode.py
│   │   ├── grist.py
│   │   ├── identifiers.py
│   │   ├── import_actions.py
│   │   ├── imports/
│   │   │   ├── __init__.py
│   │   │   ├── fixtures/
│   │   │   │   ├── nyc_schools_progress_report_ec_2013.xlsx
│   │   │   │   ├── strange_dates.xlsx
│   │   │   │   ├── test_boolean.xlsx
│   │   │   │   ├── test_empty_rows.xlsx
│   │   │   │   ├── test_encoding_utf8.csv
│   │   │   │   ├── test_excel.xlsx
│   │   │   │   ├── test_excel_numeric_gs.xlsx
│   │   │   │   ├── test_excel_types.csv
│   │   │   │   ├── test_excel_types.xlsx
│   │   │   │   ├── test_falsy_cells.xlsx
│   │   │   │   ├── test_headers_with_none_cell.xlsx
│   │   │   │   ├── test_import_csv.csv
│   │   │   │   ├── test_invalid_dimensions.xlsx
│   │   │   │   ├── test_isdigit.csv
│   │   │   │   ├── test_long_cell.csv
│   │   │   │   └── test_single_merged_cell.xlsx
│   │   │   ├── import_csv.py
│   │   │   ├── import_csv_test.py
│   │   │   ├── import_json.py
│   │   │   ├── import_json_test.py
│   │   │   ├── import_utils.py
│   │   │   ├── import_xls.py
│   │   │   ├── import_xls_test.py
│   │   │   ├── register.py
│   │   │   └── test_imports.py
│   │   ├── lookup.py
│   │   ├── main.py
│   │   ├── match_counter.py
│   │   ├── migrations.py
│   │   ├── moment.py
│   │   ├── objtypes.py
│   │   ├── parse_data.py
│   │   ├── predicate_formula.py
│   │   ├── records.py
│   │   ├── relabeling.py
│   │   ├── relation.py
│   │   ├── reverse_references.py
│   │   ├── runtests.py
│   │   ├── sandbox.py
│   │   ├── schema.py
│   │   ├── sort_key.py
│   │   ├── sort_specs.py
│   │   ├── summary.py
│   │   ├── table.py
│   │   ├── table_data_set.py
│   │   ├── test_acl_formula.py
│   │   ├── test_acl_renames.py
│   │   ├── test_actions.py
│   │   ├── test_codebuilder.py
│   │   ├── test_column_actions.py
│   │   ├── test_completion.py
│   │   ├── test_date_types.py
│   │   ├── test_default_formulas.py
│   │   ├── test_depend.py
│   │   ├── test_derived.py
│   │   ├── test_display_cols.py
│   │   ├── test_docmodel.py
│   │   ├── test_dropdown_condition.py
│   │   ├── test_dropdown_condition_renames.py
│   │   ├── test_engine.py
│   │   ├── test_find_col.py
│   │   ├── test_formula_error.py
│   │   ├── test_formula_prompt.py
│   │   ├── test_formula_undo.py
│   │   ├── test_functions.py
│   │   ├── test_gencode.py
│   │   ├── test_import_actions.py
│   │   ├── test_lookup_find.py
│   │   ├── test_lookup_perf.py
│   │   ├── test_lookup_sort.py
│   │   ├── test_lookups.py
│   │   ├── test_match_counter.py
│   │   ├── test_migrations.py
│   │   ├── test_moment.py
│   │   ├── test_objtypes.py
│   │   ├── test_predicate_formula.py
│   │   ├── test_prevnext.py
│   │   ├── test_record_func.py
│   │   ├── test_recordlist.py
│   │   ├── test_reflist_rel.py
│   │   ├── test_relabeling.py
│   │   ├── test_renames.py
│   │   ├── test_renames2.py
│   │   ├── test_replace_table_data.py
│   │   ├── test_replay.py
│   │   ├── test_requests.py
│   │   ├── test_rules.py
│   │   ├── test_rules_grid.py
│   │   ├── test_side_effects.py
│   │   ├── test_sort_key.py
│   │   ├── test_sort_spec.py
│   │   ├── test_summary.py
│   │   ├── test_summary2.py
│   │   ├── test_summary_choicelist.py
│   │   ├── test_summary_undo.py
│   │   ├── test_table_actions.py
│   │   ├── test_table_data_set.py
│   │   ├── test_temp_rowids.py
│   │   ├── test_textbuilder.py
│   │   ├── test_treeview.py
│   │   ├── test_trigger_expression.py
│   │   ├── test_trigger_formulas.py
│   │   ├── test_twoway_refs.py
│   │   ├── test_twowaymap.py
│   │   ├── test_types.py
│   │   ├── test_undo.py
│   │   ├── test_urllib_patch.py
│   │   ├── test_user.py
│   │   ├── test_useractions.py
│   │   ├── testsamples.py
│   │   ├── testscript.json
│   │   ├── testutil.py
│   │   ├── textbuilder.py
│   │   ├── timing.py
│   │   ├── treeview.py
│   │   ├── trigger_expression.py
│   │   ├── twowaymap.py
│   │   ├── tzdata.data
│   │   ├── urllib_patch.py
│   │   ├── user.py
│   │   ├── useractions.py
│   │   ├── usercode.py
│   │   ├── usertypes.py
│   │   └── xmlrunner.py
│   ├── gvisor/
│   │   ├── get_checkpoint_path.sh
│   │   ├── run.py
│   │   └── update_engine_checkpoint.sh
│   ├── install_tz.js
│   ├── pyodide/
│   │   ├── Makefile
│   │   ├── README.md
│   │   ├── build_packages.sh
│   │   ├── package.json
│   │   ├── package_filenames.json
│   │   ├── packages.js
│   │   ├── pipe.js
│   │   ├── preparePackages.js
│   │   └── setup.sh
│   ├── requirements.in
│   ├── requirements.txt
│   ├── run.sh
│   ├── setup.py
│   ├── supervisor.mjs
│   └── watch.sh
├── static/
│   ├── apiconsole.html
│   ├── app.html
│   ├── custom-widget.html
│   ├── custom.css
│   ├── error.html
│   ├── form.html
│   ├── icons/
│   │   ├── grist.icns
│   │   ├── gristdoc.icns
│   │   ├── icons.css
│   │   └── locales/
│   │       └── LICENSE
│   ├── index.html
│   ├── locales/
│   │   ├── ar.client.json
│   │   ├── ar.server.json
│   │   ├── bci.client.json
│   │   ├── bci.server.json
│   │   ├── bg.client.json
│   │   ├── bg.server.json
│   │   ├── ca.client.json
│   │   ├── ca.server.json
│   │   ├── cs.client.json
│   │   ├── cs.server.json
│   │   ├── de.client.json
│   │   ├── de.server.json
│   │   ├── el.client.json
│   │   ├── el.server.json
│   │   ├── en.client.json
│   │   ├── en.server.json
│   │   ├── en_GB.client.json
│   │   ├── en_GB.server.json
│   │   ├── es.client.json
│   │   ├── es.server.json
│   │   ├── eu.client.json
│   │   ├── eu.server.json
│   │   ├── fa.client.json
│   │   ├── fa.server.json
│   │   ├── fi.client.json
│   │   ├── fi.server.json
│   │   ├── fr.client.json
│   │   ├── fr.server.json
│   │   ├── hu.client.json
│   │   ├── hu.server.json
│   │   ├── id.client.json
│   │   ├── id.server.json
│   │   ├── ig.client.json
│   │   ├── ig.server.json
│   │   ├── it.client.json
│   │   ├── it.server.json
│   │   ├── ja.client.json
│   │   ├── ja.server.json
│   │   ├── ko.client.json
│   │   ├── ko.server.json
│   │   ├── nb_NO.client.json
│   │   ├── nb_NO.server.json
│   │   ├── nl.client.json
│   │   ├── nl.server.json
│   │   ├── pl.client.json
│   │   ├── pl.server.json
│   │   ├── pt.client.json
│   │   ├── pt.server.json
│   │   ├── pt_BR.client.json
│   │   ├── pt_BR.server.json
│   │   ├── ro.client.json
│   │   ├── ro.server.json
│   │   ├── ru.client.json
│   │   ├── ru.server.json
│   │   ├── sk.client.json
│   │   ├── sk.server.json
│   │   ├── sl.client.json
│   │   ├── sl.server.json
│   │   ├── sv.client.json
│   │   ├── sv.server.json
│   │   ├── ta.client.json
│   │   ├── ta.server.json
│   │   ├── th.client.json
│   │   ├── th.server.json
│   │   ├── tr.client.json
│   │   ├── tr.server.json
│   │   ├── uk.client.json
│   │   ├── uk.server.json
│   │   ├── ur.client.json
│   │   ├── ur.server.json
│   │   ├── vi.client.json
│   │   ├── vi.server.json
│   │   ├── zh_Hans.client.json
│   │   ├── zh_Hans.server.json
│   │   ├── zh_Hant.client.json
│   │   ├── zh_Hant.server.json
│   │   ├── zun.client.json
│   │   └── zun.server.json
│   ├── message.html
│   ├── swagger-ui-dark.css
│   ├── test.html
│   └── testWebdriverJQuery.html
├── stubs/
│   └── app/
│       ├── client/
│       │   ├── components/
│       │   │   └── Banners.ts
│       │   ├── ui/
│       │   │   ├── ActivationPage.ts
│       │   │   ├── AdminControls.ts
│       │   │   ├── BillingPage.ts
│       │   │   ├── ChangePasswordDialog.ts
│       │   │   ├── CustomThemes.ts
│       │   │   ├── DeleteAccountDialog.ts
│       │   │   ├── HomeImports.ts
│       │   │   ├── MFAConfig.ts
│       │   │   ├── NewDocMethods.ts
│       │   │   ├── Notifications.ts
│       │   │   └── ProductUpgrades.ts
│       │   └── widgets/
│       │       └── AssistantPopup.ts
│       ├── common/
│       │   └── version.ts
│       ├── server/
│       │   ├── declarations.d.ts
│       │   ├── lib/
│       │   │   ├── create.ts
│       │   │   ├── globalConfig.ts
│       │   │   └── loginSystems.ts
│       │   ├── prometheus-exporter.ts
│       │   └── server.ts
│       └── tsconfig.json
├── test/
│   ├── assistant/
│   │   ├── data/
│   │   │   └── formula-dataset-index.csv
│   │   └── v1/
│   │       ├── runCompletion.js
│   │       └── runCompletion_impl.ts
│   ├── chai-as-promised.js
│   ├── client/
│   │   ├── clientUtil.js
│   │   ├── components/
│   │   │   ├── Layout.js
│   │   │   ├── WidgetFrame.ts
│   │   │   ├── commands.js
│   │   │   └── sampleLayout.js
│   │   ├── lib/
│   │   │   ├── ACIndex.ts
│   │   │   ├── Delay.js
│   │   │   ├── DocSchemaImport.ts
│   │   │   ├── ImportSourceElement.ts
│   │   │   ├── ObservableMap.js
│   │   │   ├── ObservableSet.js
│   │   │   ├── PluginApi.ts
│   │   │   ├── SafeBrowser.ts
│   │   │   ├── Signal.ts
│   │   │   ├── UrlState.ts
│   │   │   ├── chartUtil.ts
│   │   │   ├── dispose.js
│   │   │   ├── dom.js
│   │   │   ├── domAsync.ts
│   │   │   ├── koArray.js
│   │   │   ├── koArrayWrap.ts
│   │   │   ├── koDom.js
│   │   │   ├── koDomScrolly.js
│   │   │   ├── koForm.js
│   │   │   ├── koUtil.js
│   │   │   ├── localStorageObs.ts
│   │   │   ├── localization.ts
│   │   │   ├── nameUtils.ts
│   │   │   ├── sanitizeUrl.ts
│   │   │   ├── sortUtil.ts
│   │   │   ├── textUtils.ts
│   │   │   ├── timeUtils.ts
│   │   │   └── urlUtils.ts
│   │   ├── models/
│   │   │   ├── ColumnFilter.ts
│   │   │   ├── TreeModel.ts
│   │   │   ├── gristUrlState.ts
│   │   │   ├── modelUtil.js
│   │   │   ├── rowset.js
│   │   │   └── rowuid.js
│   │   ├── shortcuts/
│   │   │   ├── excel.js
│   │   │   └── gsMac.js
│   │   ├── ui/
│   │   │   ├── DocumentSettings.ts
│   │   │   ├── RelativeDatesOptions.ts
│   │   │   └── UserImage.ts
│   │   └── ui2018/
│   │       └── cssVars.ts
│   ├── client-harness/
│   │   └── client.js
│   ├── common/
│   │   ├── ACLPermissions.ts
│   │   ├── AsyncCreate.ts
│   │   ├── BigInt.ts
│   │   ├── BinaryIndexedTree.js
│   │   ├── ChoiceListParser.ts
│   │   ├── CircularArray.js
│   │   ├── ColumnFilterFunc.ts
│   │   ├── DocActions.ts
│   │   ├── DocSchemaImport.ts
│   │   ├── InactivityTimer.ts
│   │   ├── Interval.ts
│   │   ├── KeyedMutex.ts
│   │   ├── MemBuffer.js
│   │   ├── NumberFormat.ts
│   │   ├── NumberParse.ts
│   │   ├── PluginInstance.ts
│   │   ├── RecentItems.js
│   │   ├── RefCountMap.ts
│   │   ├── RelativeDates.ts
│   │   ├── SortFunc.ts
│   │   ├── StringUnion.ts
│   │   ├── TableData.ts
│   │   ├── Telemetry.ts
│   │   ├── ThemePrefs.ts
│   │   ├── ValueFormatter.ts
│   │   ├── ValueGuesser.ts
│   │   ├── airtable/
│   │   │   ├── AirtableAPI.ts
│   │   │   ├── AirtableDataImporter.ts
│   │   │   └── AirtableSchemaImporter.ts
│   │   ├── arraySplice.js
│   │   ├── csvFormat.ts
│   │   ├── getTableTitle.ts
│   │   ├── gristUrls.ts
│   │   ├── gutil.js
│   │   ├── gutil2.ts
│   │   ├── marshal.js
│   │   ├── parseDate.ts
│   │   ├── promises.js
│   │   ├── roles.ts
│   │   ├── serializeTiming.js
│   │   ├── sortTiming.js
│   │   ├── timeFormat.js
│   │   └── tsvFormat.ts
│   ├── declarations.d.ts
│   ├── deployment/
│   │   ├── ActionLog.ts
│   │   ├── ChoiceList.ts
│   │   ├── DuplicateDocument.ts
│   │   ├── Fork.ts
│   │   ├── HomeIntro.ts
│   │   ├── Pages.ts
│   │   ├── README.md
│   │   ├── ReferenceColumns.ts
│   │   ├── ReferenceList.ts
│   │   └── Smoke.ts
│   ├── fixtures/
│   │   ├── docs/
│   │   │   ├── ACL-Test.grist
│   │   │   ├── ActiveDoc-sqlite.grist
│   │   │   ├── AllColumns.grist
│   │   │   ├── ApiDataRecordsTest.grist
│   │   │   ├── AttachmentsJsonMigration.grist
│   │   │   ├── BadRules.grist
│   │   │   ├── BlobMigrationV1.grist
│   │   │   ├── BlobMigrationV16.grist
│   │   │   ├── BlobMigrationV17.grist
│   │   │   ├── BlobMigrationV2.grist
│   │   │   ├── BlobMigrationV3.grist
│   │   │   ├── BlobMigrationV4.grist
│   │   │   ├── BlobMigrationV5.grist
│   │   │   ├── BlobMigrationV6.grist
│   │   │   ├── BlobMigrationV7.grist
│   │   │   ├── BlobMigrationV8.grist
│   │   │   ├── BlobMigrationV9.grist
│   │   │   ├── CCTransactions.grist
│   │   │   ├── CC_Statement.grist
│   │   │   ├── CC_Summaries-v2.grist
│   │   │   ├── CC_Summaries-v6.grist
│   │   │   ├── CC_Summaries.grist
│   │   │   ├── CardView.grist
│   │   │   ├── ChartData.grist
│   │   │   ├── Class Enrollment.grist
│   │   │   ├── Comments_44.grist
│   │   │   ├── CopyOptions.grist
│   │   │   ├── CopyPaste.grist
│   │   │   ├── CopyPaste2.grist
│   │   │   ├── Countries-Print.grist
│   │   │   ├── Covid-19.grist
│   │   │   ├── Currencies.grist
│   │   │   ├── CursorWithRefLists1.grist
│   │   │   ├── CustomWidget.grist
│   │   │   ├── DefaultValuesV5.grist
│   │   │   ├── DefaultValuesV6.grist
│   │   │   ├── DefaultValuesV7.grist
│   │   │   ├── DefaultValuesV8.grist
│   │   │   ├── DefaultValuesV9.grist
│   │   │   ├── DeleteColumnsUndo.grist
│   │   │   ├── DownmigrateTest.grist
│   │   │   ├── DropdownCondition.grist
│   │   │   ├── Excel.grist
│   │   │   ├── ExemptFromFilterBug.grist
│   │   │   ├── Exports.grist
│   │   │   ├── ExternalAttachmentsInvalidStoreId.grist
│   │   │   ├── Favorite_Films.grist
│   │   │   ├── Favorite_Films_Raw.grist
│   │   │   ├── Favorite_Films_With_Linked_Ref.grist
│   │   │   ├── FetchSelectedOptions.grist
│   │   │   ├── FieldSettings.grist
│   │   │   ├── FilmsWithImages.grist
│   │   │   ├── FilterByComplexCellValues.grist
│   │   │   ├── FilterLinkChain.grist
│   │   │   ├── FilterTest.grist
│   │   │   ├── Grist Basics.grist
│   │   │   ├── GristNewUserInfo.grist
│   │   │   ├── Hello.grist
│   │   │   ├── Hooks-v37.grist
│   │   │   ├── ImportReferences.grist
│   │   │   ├── InvalidValues.grist
│   │   │   ├── Investment Research (smaller).grist
│   │   │   ├── Investment Research.grist
│   │   │   ├── Landlord.grist
│   │   │   ├── LastPosition.grist
│   │   │   ├── LinkChain.grist
│   │   │   ├── LongList.grist
│   │   │   ├── ManyRefs.grist
│   │   │   ├── Memos-v34.grist
│   │   │   ├── NumericFormatting.grist
│   │   │   ├── Pages-v19.grist
│   │   │   ├── Pages.grist
│   │   │   ├── PasteParsing.grist
│   │   │   ├── RawSummaryTables.grist
│   │   │   ├── Ref-AC-Test.grist
│   │   │   ├── Ref-List-AC-Test.grist
│   │   │   ├── RemoveTransformColumns.grist
│   │   │   ├── SchoolsSample.grist
│   │   │   ├── SelectByRefList.grist
│   │   │   ├── SelectBySummary.grist
│   │   │   ├── SelectBySummaryRef.grist
│   │   │   ├── SelectionSummary.grist
│   │   │   ├── ShiftSelection.grist
│   │   │   ├── SortDates.grist
│   │   │   ├── SortFilterIconTest.grist
│   │   │   ├── SummarizeByRef.grist
│   │   │   ├── SummaryRulesBug.grist
│   │   │   ├── SummaryTableFormula.grist
│   │   │   ├── TabBar.grist
│   │   │   ├── Teams.grist
│   │   │   ├── TypeConversions.grist
│   │   │   ├── TypeEncoding.grist
│   │   │   ├── Widgets.grist
│   │   │   ├── World-v0.grist
│   │   │   ├── World-v1.grist
│   │   │   ├── World-v10.grist
│   │   │   ├── World-v11.grist
│   │   │   ├── World-v12.grist
│   │   │   ├── World-v13.grist
│   │   │   ├── World-v14.grist
│   │   │   ├── World-v15.grist
│   │   │   ├── World-v18.grist
│   │   │   ├── World-v20.grist
│   │   │   ├── World-v24.grist
│   │   │   ├── World-v25.grist
│   │   │   ├── World-v3.grist
│   │   │   ├── World-v33.grist
│   │   │   ├── World-v39.grist
│   │   │   ├── World-v8.grist
│   │   │   ├── World.grist
│   │   │   ├── WorldSQLDB.grist
│   │   │   ├── WorldUndo.grist
│   │   │   ├── doctour.grist
│   │   │   ├── selectBy.grist
│   │   │   └── video/
│   │   │       ├── ACME Orders.grist
│   │   │       ├── Afterschool Program.grist
│   │   │       ├── Candidates.grist
│   │   │       ├── Employees HomePage.grist
│   │   │       ├── Employees.grist
│   │   │       ├── Leases.grist
│   │   │       └── Lightweight CRM.grist
│   │   ├── export-csv/
│   │   │   ├── CCTransactions-DBA-desc.csv
│   │   │   ├── CCTransactions.csv
│   │   │   ├── choice.csv
│   │   │   ├── date.csv
│   │   │   ├── datetime.csv
│   │   │   ├── field-options.csv
│   │   │   ├── filtered-ref-list.csv
│   │   │   ├── filters-manual.csv
│   │   │   ├── filters-saved.csv
│   │   │   ├── hidden-text.csv
│   │   │   ├── integer.csv
│   │   │   ├── many-rows.csv
│   │   │   ├── numeric.csv
│   │   │   ├── order-color-desc.csv
│   │   │   ├── order-color-manual.csv
│   │   │   ├── order-color-place.csv
│   │   │   ├── order-manual.csv
│   │   │   ├── reference.csv
│   │   │   ├── text.csv
│   │   │   └── toggle.csv
│   │   ├── export-dsv/
│   │   │   ├── CCTransactions.dsv
│   │   │   └── text.dsv
│   │   ├── export-tsv/
│   │   │   ├── CCTransactions.tsv
│   │   │   └── text.tsv
│   │   ├── export-xlsx/
│   │   │   ├── CC_Statement.xlsx
│   │   │   ├── CC_Summaries.xlsx
│   │   │   ├── Currencies.xlsx
│   │   │   ├── Excel.xlsx
│   │   │   ├── Exports.xlsx
│   │   │   └── World-v0.xlsx
│   │   ├── plugins/
│   │   │   ├── .jshintrc
│   │   │   ├── browserInstalledPlugins/
│   │   │   │   └── plugins/
│   │   │   │       ├── browser-GristDocAPI/
│   │   │   │       │   ├── main.js
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── custom-section/
│   │   │   │       │   ├── index-bis.html
│   │   │   │       │   ├── index.html
│   │   │   │       │   ├── main.js
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   ├── test-subscribe-api.html
│   │   │   │       │   └── test-subscribe-api.js
│   │   │   │       └── dummy-importer/
│   │   │   │           ├── index.html
│   │   │   │           ├── main.js
│   │   │   │           ├── manifest.yml
│   │   │   │           ├── node/
│   │   │   │           │   └── main.js
│   │   │   │           ├── sandbox/
│   │   │   │           │   └── main.py
│   │   │   │           └── script.js
│   │   │   ├── builtInPlugins/
│   │   │   │   └── plugins/
│   │   │   │       ├── 2/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── experimental-plugin/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── invalid-contrib-point/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── long-call/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── missing-component/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── missing-safePython/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── safePython-deactivate-fast/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── testing-function-call-plugin/
│   │   │   │       │   ├── backend.js
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── valid-file-parser/
│   │   │   │       │   ├── manifest.yml
│   │   │   │       │   └── sandbox/
│   │   │   │       │       └── main.py
│   │   │   │       ├── valid-import-source/
│   │   │   │       │   └── manifest.yml
│   │   │   │       ├── wrong-json/
│   │   │   │       │   └── manifest.json
│   │   │   │       └── wrong-yaml/
│   │   │   │           └── manifest.yml
│   │   │   └── installedPlugins/
│   │   │       └── plugins/
│   │   │           ├── node-GristDocAPI/
│   │   │           │   ├── TestSubscribe.js
│   │   │           │   ├── main.js
│   │   │           │   └── manifest.yml
│   │   │           ├── node-fail/
│   │   │           │   ├── main.js
│   │   │           │   └── manifest.yml
│   │   │           ├── node-mini-csv/
│   │   │           │   ├── manifest.yml
│   │   │           │   └── nodebox/
│   │   │           │       └── main.js
│   │   │           ├── node-wrong-message/
│   │   │           │   ├── main.js
│   │   │           │   └── manifest.yml
│   │   │           └── valid-import-source/
│   │   │               └── manifest.yml
│   │   ├── projects/
│   │   │   ├── AddNewButton.ts
│   │   │   ├── ApiKey.ts
│   │   │   ├── ColorSelect.ts
│   │   │   ├── ColumnFilterMenu.ts
│   │   │   ├── DocMenu.ts
│   │   │   ├── DocumentSettings.ts
│   │   │   ├── ErrorNotify.ts
│   │   │   ├── Icons.ts
│   │   │   ├── Importer.ts
│   │   │   ├── Mentions.ts
│   │   │   ├── MultiSelector.ts
│   │   │   ├── OnBoardingPopups.ts
│   │   │   ├── PagePanels.ts
│   │   │   ├── PageWidgetPicker.ts
│   │   │   ├── PagesComponent.ts
│   │   │   ├── ParseOptions.ts
│   │   │   ├── ProgressIndicator.ts
│   │   │   ├── Selects.ts
│   │   │   ├── TreeViewComponent.ts
│   │   │   ├── UI2018.ts
│   │   │   ├── UserImage.ts
│   │   │   ├── UserManager.ts
│   │   │   ├── contextMenu.ts
│   │   │   ├── editableLabel.ts
│   │   │   ├── forms.ts
│   │   │   ├── helpers/
│   │   │   │   ├── MockUserAPI.ts
│   │   │   │   ├── Pages.ts
│   │   │   │   ├── ParseOptionsData.ts
│   │   │   │   ├── States.ts
│   │   │   │   ├── gristStyles.ts
│   │   │   │   ├── widgetPicker.ts
│   │   │   │   └── withLocale.ts
│   │   │   ├── modals.ts
│   │   │   ├── mouseDrag.ts
│   │   │   ├── resizeHandle.ts
│   │   │   ├── searchDropdown.ts
│   │   │   ├── sessionObs.ts
│   │   │   ├── simpleList.ts
│   │   │   ├── template.html
│   │   │   ├── tokenfield.ts
│   │   │   ├── tooltips.ts
│   │   │   ├── transitions.ts
│   │   │   ├── webpack-test-server.ts
│   │   │   └── webpack.config.js
│   │   ├── saml/
│   │   │   ├── keycloak.pem
│   │   │   ├── saml-login
│   │   │   ├── saml-logout
│   │   │   ├── saml.crt
│   │   │   └── saml.key
│   │   ├── sites/
│   │   │   ├── config/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── deferred-ready/
│   │   │   │   └── index.html
│   │   │   ├── embed/
│   │   │   │   └── embed.html
│   │   │   ├── fetchSelectedOptions/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── filter/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── hello/
│   │   │   │   └── index.html
│   │   │   ├── paste/
│   │   │   │   └── paste.html
│   │   │   ├── probe/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── readout/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── types/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── types-raw-refs/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   ├── types-rest-api/
│   │   │   │   ├── index.html
│   │   │   │   └── page.js
│   │   │   └── zap/
│   │   │       ├── index.html
│   │   │       └── page.js
│   │   └── uploads/
│   │       ├── BooleanData.xlsx
│   │       ├── CCTransactions.csv
│   │       ├── ChartData-Sort_Test.csv
│   │       ├── ChartData.csv
│   │       ├── Cities.csv
│   │       ├── CodeEditor.test.csv
│   │       ├── ColumnFilterData_A.csv
│   │       ├── ColumnFilterData_B.csv
│   │       ├── DateTimeData.xlsx
│   │       ├── EmptyDate.csv
│   │       ├── FileUploadData.csv
│   │       ├── ImportReferences-Tasks.csv
│   │       ├── SchoolData.csv
│   │       ├── StudentData.csv
│   │       ├── UploadedData1.csv
│   │       ├── UploadedData1Extended.csv
│   │       ├── UploadedData2.csv
│   │       ├── UploadedData2Extended.csv
│   │       ├── UploadedData3.csv
│   │       ├── UploadedDataEmpty.csv
│   │       ├── World-v0.xlsx
│   │       ├── World-v1.xlsx
│   │       ├── cities.jgrist
│   │       ├── cities_broken.jgrist
│   │       ├── dirtyNames.json
│   │       ├── empty_data.jgrist
│   │       ├── empty_excel.xlsx
│   │       ├── formatted_numbers.csv
│   │       ├── homicide_rates.xlsx
│   │       ├── htmlfile.html
│   │       ├── mixed_dates.csv
│   │       ├── name_references.csv
│   │       ├── names.json
│   │       ├── simple_array.json
│   │       ├── spotifyGetSeveralAlbums.json
│   │       ├── unicode_headers.csv
│   │       ├── unicode_headers.xlsx
│   │       └── video/
│   │           └── investment-data.xlsx
│   ├── gen-server/
│   │   ├── ApiServer.ts
│   │   ├── ApiServerAccess.ts
│   │   ├── ApiServerBenchmark.ts
│   │   ├── ApiServerBugs.ts
│   │   ├── ApiSession.ts
│   │   ├── AuthCaching.ts
│   │   ├── SqliteSettings.ts
│   │   ├── UpdateChecks.ts
│   │   ├── apiUtils.ts
│   │   ├── lib/
│   │   │   ├── DocApiForwarder.ts
│   │   │   ├── DocPrefs.ts
│   │   │   ├── DocWorkerMap.ts
│   │   │   ├── HealthCheck.ts
│   │   │   ├── HomeDBCaches.ts
│   │   │   ├── HomeDBManager.ts
│   │   │   ├── Housekeeper.ts
│   │   │   ├── emails.ts
│   │   │   ├── everyone.ts
│   │   │   ├── homedb/
│   │   │   │   ├── GroupsManager.ts
│   │   │   │   └── UsersManager.ts
│   │   │   ├── limits.ts
│   │   │   ├── listing.ts
│   │   │   ├── mergedOrgs.ts
│   │   │   ├── prefs.ts
│   │   │   ├── previewer.ts
│   │   │   ├── removedAt.ts
│   │   │   ├── scrubUserFromOrg.ts
│   │   │   ├── suspension.ts
│   │   │   └── urlIds.ts
│   │   ├── migrations.ts
│   │   ├── seed.ts
│   │   └── testUtils.ts
│   ├── init-mocha-webdriver.js
│   ├── nbrowser/
│   │   ├── AccessRules1.ts
│   │   ├── AccessRules2.ts
│   │   ├── AccessRules3.ts
│   │   ├── AccessRules4.ts
│   │   ├── AccessRulesAttrs.ts
│   │   ├── AccessRulesIntro.ts
│   │   ├── AccessRulesSchemaEdit.ts
│   │   ├── AccessibilityModal.ts
│   │   ├── ActionLog.ts
│   │   ├── ActiveUserList.ts
│   │   ├── AdminPanel.ts
│   │   ├── AdminPanelTools.ts
│   │   ├── AirtableImport.ts
│   │   ├── ApiConsole.ts
│   │   ├── AttachedCustomWidget.ts
│   │   ├── AttachmentsLinking.ts
│   │   ├── AttachmentsTransfer.ts
│   │   ├── AttachmentsWidget.ts
│   │   ├── AuthProvider.ts
│   │   ├── AuthProviderGetGrist.ts
│   │   ├── BehavioralPrompts.ts
│   │   ├── Boot.ts
│   │   ├── BundleActions.ts
│   │   ├── CardView.ts
│   │   ├── CellColor.ts
│   │   ├── CellFormat.ts
│   │   ├── ChartView1.ts
│   │   ├── Choice.ts
│   │   ├── ChoiceList.ts
│   │   ├── ClientUnitTests.ntest.js
│   │   ├── CodeEditor.ntest.js
│   │   ├── ColumnFilterMenu.ts
│   │   ├── ColumnFilterMenu2.ts
│   │   ├── ColumnFilterMenu3.ts
│   │   ├── ColumnOps.ntest.js
│   │   ├── ColumnTransform.ts
│   │   ├── Comments.ts
│   │   ├── Comparison.ts
│   │   ├── CopyPaste.ts
│   │   ├── CopyPaste2.ntest.js
│   │   ├── CopyPasteColumnOptions.ts
│   │   ├── CopyPasteFiles.ts
│   │   ├── CopyPasteLinked.ts
│   │   ├── CopyWithHeaders.ts
│   │   ├── CursorSaving.ts
│   │   ├── CustomView.ts
│   │   ├── CustomWidgets.ts
│   │   ├── CustomWidgetsConfig.ts
│   │   ├── DateEditor.ts
│   │   ├── Dates.ntest.js
│   │   ├── DeleteColumnsUndo.ts
│   │   ├── DescriptionColumn.ts
│   │   ├── DescriptionWidget.ts
│   │   ├── DetailView.ntest.js
│   │   ├── DetailView.ts
│   │   ├── DocTour.ts
│   │   ├── DocTutorial.ts
│   │   ├── DocTypeConversion.ts
│   │   ├── DocUsageTracking.ts
│   │   ├── DropdownConditionEditor.ts
│   │   ├── DuplicateDocument.ts
│   │   ├── DuplicatePage.ts
│   │   ├── Export.ntest.js
│   │   ├── ExportSection.ts
│   │   ├── Features.ts
│   │   ├── FieldConfigTab.ntest.js
│   │   ├── FieldEditor.ts
│   │   ├── FieldSettings.ntest.js
│   │   ├── FieldSettings2.ts
│   │   ├── FillLinkedRecords.ntest.js
│   │   ├── FillSelectionDown.ts
│   │   ├── FilterLinkChain.ts
│   │   ├── FilteringBugs.ts
│   │   ├── Fork.ts
│   │   ├── FormView1.ts
│   │   ├── FormView2.ts
│   │   ├── FormsUrlValues.ts
│   │   ├── FormulaAutocomplete.ts
│   │   ├── Formulas.ts
│   │   ├── GridOptions.ntest.js
│   │   ├── GridViewBugs.ts
│   │   ├── GridViewNewColumnMenu.ts
│   │   ├── GridViewNewColumnMenuDateHelpers.ts
│   │   ├── GridViewNewColumnMenuUtils.ts
│   │   ├── HeaderColor.ts
│   │   ├── Health.ntest.js
│   │   ├── HomeIntro.ts
│   │   ├── HomeIntroWithoutPlaygound.ts
│   │   ├── ImportReferences.ts
│   │   ├── Importer.ts
│   │   ├── Importer2.ts
│   │   ├── LanguageSettings.ts
│   │   ├── LazyLoad.ts
│   │   ├── LeftPanel.ts
│   │   ├── LinkingBidirectional.ts
│   │   ├── LinkingErrors.ts
│   │   ├── LinkingSelector.ts
│   │   ├── Localization.ts
│   │   ├── MultiColumn.ts
│   │   ├── NewDocument.ntest.js
│   │   ├── NumericEditor.ts
│   │   ├── OnDemand.ts
│   │   ├── Pages.ts
│   │   ├── Printing.ts
│   │   ├── Properties.ntest.js
│   │   ├── ProposedChangesPage.ts
│   │   ├── RawData.ts
│   │   ├── RecordCards.ts
│   │   ├── RecordLayout.ts
│   │   ├── RefNumericChange.ts
│   │   ├── RefTransforms.ts
│   │   ├── ReferenceColumns.ts
│   │   ├── ReferenceList.ts
│   │   ├── RegionFocusSwitcher.ts
│   │   ├── RemoveTransformColumns.ts
│   │   ├── RightPanel.ts
│   │   ├── RightPanelSelectBy.ts
│   │   ├── RowHeights.ts
│   │   ├── RowMenu.ts
│   │   ├── SavePosition.ntest.js
│   │   ├── Search.ts
│   │   ├── Search2.ts
│   │   ├── Search3.ts
│   │   ├── SearchBar.ntest.ts
│   │   ├── SectionFilter.ts
│   │   ├── SelectBy.ts
│   │   ├── SelectByRefList.ts
│   │   ├── SelectByRightPanel.ts
│   │   ├── SelectBySummary.ts
│   │   ├── SelectBySummaryRef.ts
│   │   ├── SelectionSummary.ts
│   │   ├── ShiftSelection.ts
│   │   ├── Smoke.ts
│   │   ├── SortDates.ntest.js
│   │   ├── SortEditSave.ntest.js
│   │   ├── SortFilterSectionOptions.ts
│   │   ├── SortPositions.ts
│   │   ├── Summaries.ntest.js
│   │   ├── SupportGrist.ts
│   │   ├── TermsOfService.ts
│   │   ├── TextEditor.ntest.js
│   │   ├── Themes.ts
│   │   ├── Timing.ts
│   │   ├── ToggleColumns.ts
│   │   ├── TokenField.ts
│   │   ├── TwoWayReference.ts
│   │   ├── TypeChange.ntest.js
│   │   ├── UndoJumps.ntest.js
│   │   ├── UploadLimits.ts
│   │   ├── UserManager.ts
│   │   ├── UserManager2.ts
│   │   ├── VersionUpdateBanner.ts
│   │   ├── ViewConfigTab.ntest.js
│   │   ├── ViewLayout.ts
│   │   ├── ViewLayoutCollapse.ts
│   │   ├── ViewLayoutUtils.ts
│   │   ├── Views.ntest.js
│   │   ├── VisibleFieldsConfig.ts
│   │   ├── WebhookOverflow.ts
│   │   ├── WebhookPage.ts
│   │   ├── aclTestUtils.ts
│   │   ├── chartViewTestUtils.ts
│   │   ├── customUtil.ts
│   │   ├── disabledAt.ts
│   │   ├── duplicateWidget.ts
│   │   ├── elementUtils.ts
│   │   ├── externalAttachmentsHelpers.ts
│   │   ├── formTools.ts
│   │   ├── gristUtil-nbrowser.js
│   │   ├── gristUtils.ts
│   │   ├── gristWebDriverUtils.ts
│   │   ├── homeUtil.ts
│   │   ├── importerTestUtils.ts
│   │   ├── links.ts
│   │   ├── saveViewSection.ts
│   │   ├── testServer.ts
│   │   ├── testUtils.ts
│   │   ├── webdriverUtils.ts
│   │   ├── webdriverjq-nbrowser.js
│   │   └── webdriverjq.ntest.js
│   ├── nbrowser_with_stubs/
│   │   ├── CreateTeamSite.ts
│   │   └── CustomWidgets.ts
│   ├── projects/
│   │   ├── AccountWidget.ts
│   │   ├── ApiKey.ts
│   │   ├── ColorSelect.ts
│   │   ├── ColumnFilterMenu.ts
│   │   ├── ColumnFilterMenu2.ts
│   │   ├── DateRangeFilter.ts
│   │   ├── DocMenu.ts
│   │   ├── DocumentSettings.ts
│   │   ├── Icons.ts
│   │   ├── Mentions.ts
│   │   ├── MultiSelector.ts
│   │   ├── NotifyBar.ts
│   │   ├── OnBoardingPopups.ts
│   │   ├── PagePanels.ts
│   │   ├── PageWidgetPicker.ts
│   │   ├── PagesComponent.ts
│   │   ├── RangeFilter.ts
│   │   ├── TreeViewComponent.ts
│   │   ├── UI2018.ts
│   │   ├── UserManager.ts
│   │   ├── contextMenu.ts
│   │   ├── editableLabel.ts
│   │   ├── errorPages.ts
│   │   ├── filterUtils.ts
│   │   ├── modals.ts
│   │   ├── mouseDrag.ts
│   │   ├── resizeHandle.ts
│   │   ├── searchDropdown.ts
│   │   ├── sessionObs.ts
│   │   ├── simpleList.ts
│   │   ├── testUtils.ts
│   │   ├── tokenfield.ts
│   │   ├── tooltips.ts
│   │   └── transitions.ts
│   ├── report-why-tests-hang.js
│   ├── server/
│   │   ├── Comm.ts
│   │   ├── PyMomentTest.ts
│   │   ├── Sandbox.ts
│   │   ├── customUtil.ts
│   │   ├── docTools.ts
│   │   ├── generateInitialDocSql.ts
│   │   ├── gristClient.ts
│   │   ├── lib/
│   │   │   ├── ACLFormula.ts
│   │   │   ├── ACLRulesReader.ts
│   │   │   ├── AccessTokens.ts
│   │   │   ├── ActionHistory.ts
│   │   │   ├── ActionHistoryMemory.ts
│   │   │   ├── ActionSummary.ts
│   │   │   ├── ActiveDoc.ts
│   │   │   ├── ActiveDocImport.js
│   │   │   ├── ActiveDocShutdown.ts
│   │   │   ├── AppSettings.ts
│   │   │   ├── Archive.ts
│   │   │   ├── AttachmentFileManager.ts
│   │   │   ├── AttachmentStoreProvider.ts
│   │   │   ├── Authorizer.ts
│   │   │   ├── BundleActions.ts
│   │   │   ├── CommentAccess.ts
│   │   │   ├── CommentAccess2.ts
│   │   │   ├── ConfigBackendAPI.ts
│   │   │   ├── DocSnapshots.ts
│   │   │   ├── DocStorage.js
│   │   │   ├── DocStorageManager.ts
│   │   │   ├── DocStorageMigrations.ts
│   │   │   ├── DocStorageQuery.ts
│   │   │   ├── DocWorkerLoadTracker.ts
│   │   │   ├── ExportsAccessRules.ts
│   │   │   ├── ExternalStorageAttachmentStore.ts
│   │   │   ├── FilesystemAttachmentStore.ts
│   │   │   ├── GranularAccess.ts
│   │   │   ├── GristJobs.ts
│   │   │   ├── GristSockets.ts
│   │   │   ├── HashUtil.ts
│   │   │   ├── HostedMetadataManager.ts
│   │   │   ├── HostedStorageManager.ts
│   │   │   ├── ManyFetches.ts
│   │   │   ├── MemoryPool.ts
│   │   │   ├── MinIOExternalStorage.ts
│   │   │   ├── OIDCConfig.ts
│   │   │   ├── OnDemandActions.ts
│   │   │   ├── OpenAIAssistantV1.ts
│   │   │   ├── Proposals.ts
│   │   │   ├── ProxyAgent.ts
│   │   │   ├── PubSubCache.ts
│   │   │   ├── PubSubManager.ts
│   │   │   ├── RowAccess.ts
│   │   │   ├── SQLiteDB.ts
│   │   │   ├── SamlConfig.ts
│   │   │   ├── Scim.ts
│   │   │   ├── TableMetadataLoader.ts
│   │   │   ├── Telemetry.ts
│   │   │   ├── TestingHooks.ts
│   │   │   ├── Throttle.ts
│   │   │   ├── TimeQuery.ts
│   │   │   ├── Triggers.ts
│   │   │   ├── UnhandledErrors.ts
│   │   │   ├── UserAttributes.ts
│   │   │   ├── UserPresence.ts
│   │   │   ├── Webhooks-Proxy.ts
│   │   │   ├── checksumFile.ts
│   │   │   ├── config.ts
│   │   │   ├── configCore.ts
│   │   │   ├── configCoreFileFormats.ts
│   │   │   ├── docapi/
│   │   │   │   ├── DocApiAnonPlayground.ts
│   │   │   │   ├── DocApiAttachments.ts
│   │   │   │   ├── DocApiBugsAndFixes.ts
│   │   │   │   ├── DocApiColumns.ts
│   │   │   │   ├── DocApiCreation.ts
│   │   │   │   ├── DocApiDocuments.ts
│   │   │   │   ├── DocApiDownloads.ts
│   │   │   │   ├── DocApiMisc.ts
│   │   │   │   ├── DocApiOrgLimitFlags.ts
│   │   │   │   ├── DocApiPermissions.ts
│   │   │   │   ├── DocApiQueryParameters.ts
│   │   │   │   ├── DocApiRecords.ts
│   │   │   │   ├── DocApiReverseProxy.ts
│   │   │   │   ├── DocApiSql.ts
│   │   │   │   ├── DocApiTables.ts
│   │   │   │   ├── DocApiWebhooks.ts
│   │   │   │   └── helpers.ts
│   │   │   ├── extractOrg.ts
│   │   │   ├── helpers/
│   │   │   │   ├── PrepareDatabase.ts
│   │   │   │   ├── PrepareFilesystemDirectoryForTests.ts
│   │   │   │   ├── Signal.ts
│   │   │   │   ├── TestProxyServer.ts
│   │   │   │   └── TestServer.ts
│   │   │   ├── idUtils.ts
│   │   │   ├── requestUtils.ts
│   │   │   ├── sandboxUtil.ts
│   │   │   ├── serverUtils.js
│   │   │   ├── serverUtils2.ts
│   │   │   ├── shortDesc.js
│   │   │   ├── updateChecker.ts
│   │   │   └── uploads.ts
│   │   ├── tcpForwarder.ts
│   │   ├── testCleanup.ts
│   │   ├── testUtils.ts
│   │   ├── utils/
│   │   │   ├── CachedFetcher.ts
│   │   │   ├── LogSanitizer.ts
│   │   │   └── streams.ts
│   │   └── wait.ts
│   ├── setupPaths.js
│   ├── split-tests.js
│   ├── testUtils.ts
│   ├── test_env.sh
│   ├── test_under_docker.sh
│   ├── timings/
│   │   ├── nbrowser.txt
│   │   └── server.txt
│   ├── tsconfig.json
│   ├── upgradeDocument
│   ├── upgradeDocumentImpl.ts
│   ├── utils.js
│   └── xunit-file.js
├── tsconfig-ext.json
├── tsconfig-prod.json
├── tsconfig.eslint.json
└── tsconfig.json
Download .txt
Showing preview only (1,367K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (15508 symbols across 1420 files)

FILE: app/client/DefaultHooks.ts
  type IHooks (line 5) | interface IHooks {
  method maybeModifyLinkAttrs (line 20) | maybeModifyLinkAttrs(attrs: IAttrObj) {

FILE: app/client/aclui/ACLColumnList.ts
  function aclColumnList (line 11) | function aclColumnList(colIds: Observable<string[]>, validColIds: string...

FILE: app/client/aclui/ACLFormulaEditor.ts
  type ACLFormulaOptions (line 12) | interface ACLFormulaOptions {
  function aclFormulaEditor (line 21) | function aclFormulaEditor(options: ACLFormulaOptions) {

FILE: app/client/aclui/ACLMemoEditor.ts
  function aclMemoEditor (line 5) | function aclMemoEditor(obs: Observable<string>, ...args: DomElementArg[]...

FILE: app/client/aclui/ACLSelect.ts
  function aclSelect (line 11) | function aclSelect<T>(obs: Observable<T>, optionArray: MaybeObsArray<IOp...

FILE: app/client/aclui/ACLUsers.ts
  function isSpecialEmail (line 24) | function isSpecialEmail(email: string) {
  class ACLUsersPopup (line 28) | class ACLUsersPopup extends Disposable {
    method constructor (line 36) | constructor(public pageModel: DocPageModel,
    method load (line 41) | public async load() {
    method getUsers (line 47) | public getUsers() {
    method init (line 53) | public init(permissionData: PermissionDataWithExtraUsers | null) {
    method attachPopup (line 73) | public attachPopup(elem: Element, options: IPopupOptions & { resetDocP...
    method menu (line 101) | public menu(options: IMenuOptions) {
    method _fetchData (line 115) | private async _fetchData() {
    method _showExampleUsers (line 121) | private _showExampleUsers() {
    method _buildUserRow (line 125) | private _buildUserRow(user: UserAccessData, opt: { isExampleUser?: boo...
    method _viewAs (line 142) | private _viewAs(user: UserAccessData, resetDocPage: boolean = false) {

FILE: app/client/aclui/AccessRules.ts
  type ResourceRec (line 82) | type ResourceRec = SchemaTypes["_grist_ACLResources"] & { id?: number };
  type RuleRec (line 83) | type RuleRec = Partial<SchemaTypes["_grist_ACLRules"]> & { id?: number, ...
  type UseCB (line 85) | type UseCB = <T>(obs: BaseObservable<T>) => T;
  type RuleStatus (line 89) | enum RuleStatus {
  type ISuggestionInfo (line 96) | interface ISuggestionInfo {
  type IAttrOption (line 103) | interface IAttrOption extends ISuggestionInfo {
  type IColTypeInfo (line 108) | interface IColTypeInfo extends ISuggestionInfo {
  class Suggestion (line 112) | class Suggestion implements ISuggestionWithSubAttrs {
    method constructor (line 113) | constructor(public value: string, private _info?: ISuggestionInfo) {}
    method example (line 114) | public get example() {
    method subAttributes (line 118) | public subAttributes(): ISuggestionWithSubAttrs[] {
  class AccessRules (line 129) | class AccessRules extends Disposable {
    method constructor (line 181) | constructor(public gristDoc: GristDoc) {
    method allTableIds (line 255) | public get allTableIds() { return Array.from(this._aclResources.keys()...
    method userAttrRules (line 256) | public get userAttrRules() { return this._userAttrRules; }
    method userAttrChoices (line 257) | public get userAttrChoices() { return this._userAttrChoices; }
    method getTableTitle (line 259) | public getTableTitle(tableId: string) {
    method update (line 268) | public async update() {
    method save (line 324) | public async save(): Promise<void> {
    method buildDom (line 453) | public buildDom() {
    method _buildRulesDom (line 464) | public _buildRulesDom() {
    method _buildRuleProblemsDom (line 572) | public _buildRuleProblemsDom(ruleProblems: AclRuleProblem[]) {
    method getRules (line 595) | public getRules(): RuleRec[] {
    method removeTableRules (line 605) | public removeTableRules(tableRules: TableRules) {
    method removeUserAttributes (line 609) | public removeUserAttributes(userAttr: ObsUserAttributeRule) {
    method checkAclFormula (line 613) | public async checkAclFormula(text: string): Promise<PredicateFormulaPr...
    method checkTableColumns (line 622) | public checkTableColumns(tableId: string, colIds?: string[], exemptCol...
    method getValidColIds (line 639) | public getValidColIds(tableId: string): string[] | undefined {
    method getColTypeInfo (line 643) | public getColTypeInfo(tableId?: string): IColTypeInfo[] {
    method typeCheckFormula (line 648) | public typeCheckFormula(formulaParsed: ParsedPredicateFormula, tableId...
    method getSeedRules (line 660) | public getSeedRules(): ObsRulePart[] {
    method _buildError (line 664) | private _buildError() {
    method _buildIntro (line 675) | private _buildIntro() {
    method _confirmEnableAccessRules (line 698) | private _confirmEnableAccessRules() {
    method _doEnableAccessRules (line 714) | private _doEnableAccessRules() {
    method _confirmDisableAccessRules (line 719) | private _confirmDisableAccessRules() {
    method _doDisableAccessRules (line 734) | private _doDisableAccessRules() {
    method _getSampleRecord (line 746) | private _getSampleRecord(tableId: string): InfoView {
    method _addTableRules (line 750) | private _addTableRules(tableId: string) {
    method _addUserAttributes (line 760) | private _addUserAttributes() {
    method _onChange (line 764) | private _onChange() {
    method _updateDocAccessData (line 775) | private async _updateDocAccessData() {
    method _addButtonsForMissingTables (line 779) | private _addButtonsForMissingTables(buttons: (HTMLAnchorElement | HTML...
    method _addButtonsForMissingColumns (line 794) | private _addButtonsForMissingColumns(buttons: (HTMLAnchorElement | HTM...
    method _addButtonsForMisconfiguredUserAttributes (line 820) | private _addButtonsForMisconfiguredUserAttributes(
  class TableRules (line 838) | class TableRules extends Disposable {
    method constructor (line 851) | constructor(public readonly tableId: string, public _accessRules: Acce...
    method getCustomRules (line 884) | public getCustomRules(colId: string): ObsRulePart[] {
    method addDefaultRules (line 896) | public addDefaultRules(rules: ObsRulePart[]) {
    method remove (line 901) | public remove() {
    method columnRuleSets (line 905) | public get columnRuleSets() {
    method buildDom (line 909) | public buildDom() {
    method buildColumnRuleSets (line 943) | public buildColumnRuleSets() {
    method buildErrors (line 950) | public buildErrors() {
    method getResources (line 958) | public getResources(): ResourceRec[] {
    method getRules (line 1009) | public getRules(): RuleRec[] {
    method removeRuleSet (line 1016) | public removeRuleSet(ruleSet: ObsRuleSet) {
    method _createColumnObsRuleSet (line 1027) | protected _createColumnObsRuleSet(
    method _addColumnRuleSet (line 1034) | private _addColumnRuleSet() {
    method _addDefaultRuleSet (line 1040) | private _addDefaultRuleSet() {
  class SpecialRules (line 1052) | class SpecialRules extends TableRules {
    method buildDom (line 1053) | public buildDom() {
    method buildCheckBoxes (line 1066) | public buildCheckBoxes() {
    method getResources (line 1073) | public getResources(): ResourceRec[] {
    method setToRecommended (line 1079) | public setToRecommended() {
    method clearRules (line 1087) | public clearRules() {
    method _createColumnObsRuleSet (line 1095) | protected _createColumnObsRuleSet(
  class SpecialRulesMain (line 1103) | class SpecialRulesMain extends SpecialRules {
    method allowAccessRules (line 1107) | public get allowAccessRules() { return this._allowAccessRules; }
    method onAccessRulesToggled (line 1109) | public onAccessRulesToggled(value: boolean) {
    method _createColumnObsRuleSet (line 1113) | protected _createColumnObsRuleSet(
  class SpecialRulesTemplates (line 1132) | class SpecialRulesTemplates extends SpecialRules {
    method buildDom (line 1135) | public buildDom() {
  method constructor (line 1162) | constructor(public accessRules: AccessRules, protected _tableRules: Tabl...
  method remove (line 1182) | public remove() {
  method getRules (line 1186) | public getRules(tableId: string): RuleRec[] {
  method getColIds (line 1196) | public getColIds(): string {
  method summarizePermissions (line 1205) | public summarizePermissions(): MixedPermissionValue {
  method buildRuleSetDom (line 1211) | public buildRuleSetDom() {
  method removeRulePart (line 1236) | public removeRulePart(rulePart: ObsRulePart) {
  method addRulePart (line 1243) | public addRulePart(beforeRule: ObsRulePart | null,
  method addRuleParts (line 1261) | public addRuleParts(newParts: ObsRulePart[], options: { foldEveryoneRule...
  method getFirstBuiltIn (line 1304) | public getFirstBuiltIn(): ObsRulePart | undefined {
  method getFirst (line 1309) | public getFirst(): ObsRulePart | undefined {
  method isSoleCondition (line 1317) | public isSoleCondition(use: UseCB, part: ObsRulePart): boolean {
  method isLastCondition (line 1325) | public isLastCondition(use: UseCB, part: ObsRulePart): boolean {
  method hasDefaultCondition (line 1330) | public hasDefaultCondition(use: UseCB): boolean {
  method getDefaultCondition (line 1335) | public getDefaultCondition(): ObsRulePart | null {
  method getAvailableBits (line 1344) | public getAvailableBits(): PermissionKey[] {
  method getValidColIds (line 1351) | public getValidColIds(): string[] {
  method getColTypeInfo (line 1356) | public getColTypeInfo() { return this.accessRules.getColTypeInfo(this._t...
  method typeCheckFormula (line 1358) | public typeCheckFormula(formulaParsed: ParsedPredicateFormula) {
  method hasColumns (line 1365) | public hasColumns() {
  method hasOnlyBuiltInRules (line 1369) | public hasOnlyBuiltInRules() {
  method getCustomRules (line 1374) | public getCustomRules(): ObsRulePart[] {
  method getSpecialColumn (line 1381) | public getSpecialColumn(): string | undefined {
  class ColumnObsRuleSet (line 1389) | class ColumnObsRuleSet extends ObsRuleSet {
    method constructor (line 1395) | constructor(accessRules: AccessRules, tableRules: TableRules, ruleSet:...
    method buildResourceDom (line 1413) | public buildResourceDom(): DomElementArg {
    method getColIdList (line 1417) | public getColIdList(): string[] {
    method removeColId (line 1421) | public removeColId(colId: string) {
    method getColIds (line 1425) | public getColIds(): string {
    method getAvailableBits (line 1429) | public getAvailableBits(): PermissionKey[] {
    method hasColumns (line 1433) | public hasColumns() {
    method _getValidColIdsList (line 1437) | private _getValidColIdsList(): string[] {
  class DefaultObsRuleSet (line 1442) | class DefaultObsRuleSet extends ObsRuleSet {
    method constructor (line 1443) | constructor(accessRules: AccessRules, tableRules: TableRules | null,
    method buildResourceDom (line 1448) | public buildResourceDom() {
  type SpecialRuleBody (line 1459) | interface SpecialRuleBody {
  type SpecialRuleProperties (line 1468) | interface SpecialRuleProperties extends SpecialRuleBody {
  function getSpecialRuleProperties (line 1528) | function getSpecialRuleProperties(name: string): SpecialRuleProperties {
  class SpecialObsRuleSet (line 1536) | class SpecialObsRuleSet extends ColumnObsRuleSet {
    method props (line 1542) | public get props() {
    method isNonEmpty (line 1546) | public get isNonEmpty() { return this._isNonEmpty; }
    method buildRuleSetDom (line 1548) | public buildRuleSetDom() {
    method getAvailableBits (line 1603) | public getAvailableBits(): PermissionKey[] {
    method removeRulePart (line 1607) | public removeRulePart(rulePart: ObsRulePart) {
    method setToRecommended (line 1615) | public setToRecommended() {
    method clearRules (line 1619) | public clearRules() {
    method _buildDomWarning (line 1623) | protected _buildDomWarning(): DomContents {
    method _createIsNonStandardObs (line 1629) | protected _createIsNonStandardObs(owner: IDisposableOwner): Observable...
    method _createIsCheckedObs (line 1636) | protected _createIsCheckedObs(owner: IDisposableOwner, isNonStandard: ...
    method _setToChecked (line 1642) | protected _setToChecked(value: boolean) {
  function makeRulePart (line 1656) | function makeRulePart({ permissions, formula }: SpecialRuleBody): RulePa...
  class SpecialObsRuleSetAccessRules (line 1665) | class SpecialObsRuleSetAccessRules extends SpecialObsRuleSet {
    method constructor (line 1666) | constructor(accessRules: AccessRules, protected _tableRules: SpecialRu...
    method _setToChecked (line 1671) | public _setToChecked(value: boolean) {
  class SpecialObsRuleSetDenyCopies (line 1677) | class SpecialObsRuleSetDenyCopies extends SpecialObsRuleSet {
    method constructor (line 1678) | constructor(accessRules: AccessRules, protected _tableRules: SpecialRu...
    method onAccessRulesToggled (line 1683) | public onAccessRulesToggled(value: boolean) {
    method buildRuleSetDom (line 1689) | public buildRuleSetDom() {
  class SpecialSchemaObsRuleSet (line 1705) | class SpecialSchemaObsRuleSet extends SpecialObsRuleSet {
    method setToRecommended (line 1706) | public setToRecommended() {
    method _buildDomWarning (line 1710) | protected _buildDomWarning(): DomContents {
    method _createIsNonStandardObs (line 1725) | protected _createIsNonStandardObs(owner: IDisposableOwner): Observable...
    method _createIsCheckedObs (line 1731) | protected _createIsCheckedObs(owner: IDisposableOwner, isNonStandard: ...
    method _allowEditors (line 1739) | private _allowEditors(value: boolean | "confirm") {
  class ObsUserAttributeRule (line 1753) | class ObsUserAttributeRule extends Disposable {
    method constructor (line 1769) | constructor(private _accessRules: AccessRules, private _userAttr?: Use...
    method remove (line 1806) | public remove() {
    method name (line 1810) | public get name() { return this._name; }
    method tableId (line 1811) | public get tableId() { return this._tableId; }
    method buildUserAttrDom (line 1813) | public buildUserAttrDom() {
    method getRule (line 1871) | public getRule() {
    method _setUserAttr (line 1896) | private _setUserAttr(text: string) {
    method _getUserAttrError (line 1904) | private _getUserAttrError(text: string): string | null {
  class ObsRulePart (line 1923) | class ObsRulePart extends Disposable {
    method constructor (line 1969) | constructor(private _ruleSet: ObsRuleSet, private _rulePart?: RulePart...
    method getRulePart (line 2012) | public getRulePart(): RuleRec {
    method hasEmptyCondition (line 2024) | public hasEmptyCondition(use: UseCB): boolean {
    method matches (line 2028) | public matches(use: UseCB, aclFormula: string, permissionsText: string...
    method summarizePermissions (line 2038) | public summarizePermissions(): MixedPermissionValue {
    method sanityCheck (line 2045) | public sanityCheck(pset?: PartialPermissionSet) {
    method buildRulePartDom (line 2049) | public buildRulePartDom(wide: boolean = false) {
    method isBuiltIn (line 2146) | public isBuiltIn(): boolean {
    method isEmpty (line 2151) | public isEmpty(use: UseCB = unwrap): boolean {
    method isBuiltInOrEmpty (line 2157) | public isBuiltInOrEmpty(use: UseCB = unwrap): boolean {
    method _isNonFirstBuiltIn (line 2161) | private _isNonFirstBuiltIn(): boolean {
    method _setAclFormula (line 2165) | private async _setAclFormula(text: string, initial: boolean = false) {
    method _warnInvalidFormula (line 2181) | private _warnInvalidFormula(formulaProperties: PredicateFormulaPropert...
    method _warnInvalidColIds (line 2186) | private _warnInvalidColIds(colIds?: string[]): string | false {
    method _typeCheckFormula (line 2202) | private _typeCheckFormula(formulaParsed?: ParsedPredicateFormula): str...
  function syncRecords (line 2224) | function syncRecords(tableData: TableData, newRecords: RowRecord[],
  function getColChanges (line 2270) | function getColChanges(pairs: [RowRecord, RowRecord][]): BulkColValues {
  function serializeResource (line 2286) | function serializeResource(rec: RowRecord): string {
  function flatten (line 2290) | function flatten<T>(...args: T[][]): T[] {
  function removeItem (line 2294) | function removeItem<T>(observableArray: MutableObsArray<T>, item: T): bo...
  function getChangedStatus (line 2303) | function getChangedStatus(value: boolean): RuleStatus {
  function getAclFormulaProperties (line 2307) | function getAclFormulaProperties(part?: RulePart): PredicateFormulaPrope...
  function filterRuleSet (line 2313) | function filterRuleSet(colIds: string[], ruleSet?: RuleSet): RuleSet | u...
  function filterRuleSets (line 2324) | function filterRuleSets(colIds: string[], ruleSets: RuleSet[]): RuleSet[] {
  function makeSuggestionExample (line 2328) | function makeSuggestionExample(value: unknown): string | undefined {
  function getColTypeInfo (line 2341) | function getColTypeInfo(colIds: string[], tableData?: TableData): IColTy...
  function getSampleRecord (line 2353) | function getSampleRecord(colIds: string[], tableData?: TableData): InfoV...

FILE: app/client/aclui/PermissionsWidget.ts
  constant PERMISSION_BIT_ORDER (line 17) | const PERMISSION_BIT_ORDER = "RUCDS";
  function permissionsWidget (line 24) | function permissionsWidget(
  function next (line 90) | function next(pvalue: PartialPermissionValue): PartialPermissionValue {
  function makePermissionSet (line 99) | function makePermissionSet(bits: PermissionKey[], makeValue: (bit: Permi...
  function tick (line 108) | function tick(show: boolean) {
  function psetDescription (line 113) | function psetDescription(permissionSet: PartialPermissionSet): string {
  function sortBits (line 136) | function sortBits(bits: PermissionKey[]) {

FILE: app/client/apiconsole.ts
  function loadExternal (line 24) | function loadExternal() {
  type ParamValue (line 40) | type ParamValue = string | number | null;
  type Example (line 42) | interface Example {
  type JsonSpec (line 47) | interface JsonSpec {
  type SpecActions (line 50) | interface SpecActions {
  function applySpecActions (line 55) | function applySpecActions(cb: (specActions: SpecActions, jsonSpec: JsonS...
  function updateSpec (line 64) | function updateSpec(cb: (spec: JsonSpec) => JsonSpec) {
  function setExamples (line 74) | function setExamples(examplesArr: Example[], paramName: string) {
  function setParamValue (line 111) | function setParamValue(resolvedParam: any, value: ParamValue) {
  class ExtendedDocAPIImpl (line 143) | class ExtendedDocAPIImpl extends DocAPIImpl {
    method listTables (line 144) | public listTables(): Promise<{ tables: RecordWithStringId[] }> {
    method listColumns (line 148) | public listColumns(tableId: string, includeHidden = false): Promise<{ ...
  function wrapChangeParamByIdentity (line 153) | function wrapChangeParamByIdentity(appModel: AppModel, system: any, oriA...
  function gristPlugin (line 216) | function gristPlugin(appModel: AppModel, system: any) {
  function initialize (line 230) | function initialize(appModel: AppModel) {
  function requestInterceptor (line 302) | async function requestInterceptor(request: SwaggerUI.Request) {

FILE: app/client/components/AceEditor.js
  function AceEditor (line 25) | function AceEditor(options) {

FILE: app/client/components/AceEditorCompletions.ts
  type ICompletionOptions (line 6) | interface ICompletionOptions {
  function setupAceEditorCompletions (line 12) | function setupAceEditorCompletions(editor: Ace.Editor, options: IComplet...
  function initCustomCompleter (line 92) | function initCustomCompleter() {
  constant BASE_PADDING (line 187) | const BASE_PADDING = 8;
  constant MAX_RELATIVE_SHARED_PADDING (line 194) | const MAX_RELATIVE_SHARED_PADDING = 15;
  constant MAX_ABSOLUTE_SHARED_PADDING (line 196) | const MAX_ABSOLUTE_SHARED_PADDING = 40;
  type AceSuggestion (line 199) | interface AceSuggestion {
  function aceCompleterAddHelpLinks (line 224) | function aceCompleterAddHelpLinks(completer: any) {
  function customizeAceCompleterPopup (line 234) | function customizeAceCompleterPopup(completer: any, popup: any) {
  function retokenizeAceCompleterRow (line 252) | function retokenizeAceCompleterRow(rowData: AceSuggestion, tokens: Ace.T...
  function maybeAceCompleterLinkClick (line 331) | function maybeAceCompleterLinkClick(domEvent: Event) {

FILE: app/client/components/ActionCounter.ts
  constant MAX_MEMORY_OF_COUNTED_ACTIONS (line 8) | const MAX_MEMORY_OF_COUNTED_ACTIONS = 250;
  constant MAX_COUNT (line 9) | const MAX_COUNT = 20;
  class ActionCounter (line 22) | class ActionCounter extends dispose.Disposable {
    method create (line 52) | public create(log: MinimalActionGroup[], docData: DocData) {
    method setMark (line 101) | public setMark(state?: DocState) {
    method setMarkToBaseAction (line 107) | public setMarkToBaseAction() {
    method pushAction (line 114) | public pushAction(action: MinimalActionGroup) {
    method _countAction (line 133) | private _countAction(action: MinimalActionGroup) {
    method _changeCount (line 137) | private _changeCount(delta: number, value?: number) {
    method _setCount (line 146) | private  _setCount() {
    method _truncated (line 151) | private _truncated(value: number): number | "..." {

FILE: app/client/components/ActionLog.ts
  type ActionGroupWithState (line 42) | interface ActionGroupWithState extends ActionGroup {
  type ActionContext (line 49) | type ActionContext = Record<string, ResultRow[]>;
  class ActionLog (line 62) | class ActionLog extends dispose.Disposable implements IDomComponent {
    method create (line 80) | public create(options: { gristDoc: GristDoc | null }) {
    method buildDom (line 103) | public buildDom() {
    method getChangesSince (line 111) | public async getChangesSince(actionNum: number): Promise<ActionSummary> {
    method pushAction (line 121) | public pushAction(ag: ActionGroupWithState): void {
    method renderTabularDiffs (line 168) | public renderTabularDiffs(sum: ActionSummary, txt: string, ag?: Action...
    method _setupFilters (line 185) | private _setupFilters(ag: ActionGroupWithState, prev?: ActionGroupWith...
    method _hasSelectedTable (line 217) | private _hasSelectedTable(ag: ActionGroupWithState): boolean {
    method _buildLogDom (line 222) | private _buildLogDom() {
    method _loadActionSummaries (line 268) | private async _loadActionSummaries() {
  method constructor (line 294) | public constructor(
  method renderTabularDiffs (line 325) | public renderTabularDiffs(sum: ActionSummary, options: RenderTabularDiff...
  method toggleContext (line 378) | public async toggleContext(contextObs: ko.Observable<ActionContext>, tab...
  method _renderCell (line 395) | private _renderCell(cell: CellDelta | string | null) {
  method _renderTableName (line 433) | private _renderTableName(name: string): string {
  method _renderSchemaChange (line 453) | private _renderSchemaChange(scope: string, pair: LabelDelta) {
  method _renderTableSchemaChanges (line 468) | private _renderTableSchemaChanges(sum: ActionSummary) {
  method _renderColumnSchemaChanges (line 476) | private _renderColumnSchemaChanges(sum: ActionSummary) {
  method _resetContext (line 485) | private async _resetContext(contextObs: ko.Observable<ActionContext>, ta...
  method _setContext (line 490) | private async _setContext(contextObs: ko.Observable<ActionContext>, tabl...
  method _naiveColumnOrder (line 499) | private _naiveColumnOrder(tableId: string, colIds: string[]) {
  class ActionLogPartInList (line 527) | class ActionLogPartInList extends ActionLogPart {
    method constructor (line 528) | public constructor(
    method showForTable (line 536) | public showForTable(tableName: string): boolean {
    method selectCell (line 540) | public async selectCell(rowId: number, colId: string, tableId: string)...
    method getContext (line 565) | public async getContext(): Promise<ActionContext | undefined> {
    method _showForTable (line 587) | private _showForTable(tableName: string, ag?: ActionGroupWithState): b...
  function traceCell (line 599) | function traceCell(cell: { rowId: number, colId: string, tableId: string },
  function showCell (line 645) | async function showCell(gristDoc: GristDoc, cell: {
  function computeContext (line 681) | async function computeContext(gristDoc: GristDoc, base: ActionSummary, i...
  function reportDeletedObject (line 711) | function reportDeletedObject(obj: DeletedObject, actionNum: number) {
  type DeletedObject (line 730) | interface DeletedObject {
  type RenderTabularDiffOptions (line 736) | interface RenderTabularDiffOptions {

FILE: app/client/components/Banner.ts
  type BannerOptions (line 9) | interface BannerOptions {
  class Banner (line 71) | class Banner extends Disposable {
    method constructor (line 74) | constructor(private _options: BannerOptions) {
    method buildDom (line 78) | public buildDom() {
    method _buildContent (line 88) | private _buildContent() {
    method _buildButtons (line 99) | private _buildButtons() {
    method _buildCloseButton (line 106) | private _buildCloseButton() {
    method _buildExpandButton (line 113) | private _buildExpandButton() {
  function buildBannerMessage (line 123) | function buildBannerMessage(...domArgs: DomElementArg[]) {

FILE: app/client/components/BaseView.ts
  type ViewOptions (line 58) | interface ViewOptions {
  class BaseView (line 75) | class BaseView extends DisposableWithEvents {
    method constructor (line 115) | constructor(
    method getSelection (line 383) | protected getSelection(): CopySelection {
    method selectedRows (line 392) | protected selectedRows(): number[] {
    method deleteRows (line 396) | protected deleteRows(rowIds: number[]) {
    method deleteRecords (line 402) | protected deleteRecords(source: unknown) {
    method setCursorPos (line 443) | public setCursorPos(
    method getLoadingDonePromise (line 462) | public async getLoadingDonePromise(): Promise<void> {
    method activateEditorAtCursor (line 471) | public activateEditorAtCursor(options: BuildEditorOptions = {}): void {
    method _openDiscussionAtCursor (line 491) | private _openDiscussionAtCursor(text: CommentWithMentions | null) {
    method moveEditRowToCursor (line 515) | public moveEditRowToCursor(): DataRowModel {
    method getAnchorLinkForSection (line 522) | public getAnchorLinkForSection(sectionId: number): IGristUrlState {
    method copyLink (line 540) | protected async copyLink() {
    method filterByThisCellValue (line 546) | protected filterByThisCellValue() {
    method insertRow (line 575) | public insertRow(index?: number): Promise<number> | undefined {
    method _setCursorPosImmediately (line 597) | private _setCursorPosImmediately(cursorPos: CursorPos, isFromLink: boo...
    method _getDefaultColValues (line 602) | private _getDefaultColValues() {
    method _enhanceAction (line 610) | private _enhanceAction(action: UserAction) {
    method prepTableActions (line 628) | protected prepTableActions(actions: UserAction[]) {
    method sendTableActions (line 640) | protected sendTableActions(actions: UserAction[], optDesc?: string) {
    method sendTableAction (line 648) | protected sendTableAction(action: UserAction, optDesc?: string) {
    method insertCurrentDate (line 658) | protected insertCurrentDate(withTime: boolean) {
    method _saveEditRowField (line 703) | private _saveEditRowField(editRowModel: DataRowModel, colName: string,...
    method copy (line 752) | protected copy(selection: CopySelection) {
    method cut (line 770) | protected cut(selection: CopySelection) {
    method sendPasteActions (line 786) | protected sendPasteActions(cutCallback: CutCallback | null, actions: U...
    method buildDom (line 807) | protected buildDom() {
    method buildTitleControls (line 815) | public buildTitleControls(): DomArg {
    method onTableLoaded (line 823) | protected onTableLoaded() {
    method onResize (line 839) | public onResize(): void {
    method onRowResize (line 846) | public onRowResize(rowModels: BaseRowModel[]): void {
    method onLinkFilterChange (line 852) | protected onLinkFilterChange() {
    method prepareToPrint (line 863) | public prepareToPrint(onOff: boolean): void {
    method getRenderedRowModel (line 872) | protected getRenderedRowModel(rowId: UIRowId): DataRowModel | undefined {
    method getLastDataRowIndex (line 879) | protected getLastDataRowIndex() {
    method createFilterMenu (line 887) | public createFilterMenu(
    method isFiltered (line 906) | protected isFiltered() {
    method scrollToCursor (line 916) | public async scrollToCursor(sync?: boolean): Promise<void> {
    method _getRowInsertPos (line 925) | protected _getRowInsertPos(index: number, numInserts: number) {
    method _duplicateRows (line 934) | protected async _duplicateRows(): Promise<number[] | undefined> {
    method viewSelectedRecordAsCard (line 981) | public viewSelectedRecordAsCard(): void {
    method isRecordCardDisabled (line 991) | public isRecordCardDisabled(): boolean {

FILE: app/client/components/BaseView2.ts
  function isFileList (line 19) | function isFileList(value: unknown): value is File[] {
  function parsePasteForView (line 31) | async function parsePasteForView(
  function getDefaultColValues (line 136) | function getDefaultColValues(viewSection: ViewSectionRec): Record<string...

FILE: app/client/components/BehavioralPromptsManager.ts
  type ShowPopupOptions (line 16) | interface ShowPopupOptions {
  type AttachPopupOptions (line 26) | interface AttachPopupOptions extends ShowPopupOptions {
  type QueuedPopup (line 35) | interface QueuedPopup {
  class BehavioralPromptsManager (line 48) | class BehavioralPromptsManager extends Disposable {
    method constructor (line 63) | constructor(private _appModel: AppModel) {
    method showPopup (line 67) | public showPopup(refElement: Element, prompt: BehavioralPrompt, option...
    method attachPopup (line 71) | public attachPopup(prompt: BehavioralPrompt, options: AttachPopupOptio...
    method hasSeenPopup (line 79) | public hasSeenPopup(prompt: BehavioralPrompt) {
    method shouldShowPopup (line 83) | public shouldShowPopup(prompt: BehavioralPrompt): boolean {
    method enable (line 130) | public enable() {
    method disable (line 134) | public disable() {
    method isDisabled (line 140) | public isDisabled() {
    method reset (line 144) | public reset() {
    method _queuePopup (line 149) | private _queuePopup(refElement: Element, prompt: BehavioralPrompt, opt...
    method _showPopup (line 162) | private _showPopup(refElement: Element, prompt: BehavioralPrompt, opti...
    method _showNextQueuedPopup (line 201) | private _showNextQueuedPopup() {
    method _markAsSeen (line 210) | private _markAsSeen(prompt: BehavioralPrompt) {
    method _dontShowTips (line 219) | private _dontShowTips() {
    method _removeActivePopup (line 228) | private _removeActivePopup() {
    method _removeQueuedPopups (line 234) | private _removeQueuedPopups() {

FILE: app/client/components/CellPosition.ts
  method equals (line 9) | public static equals(a: CellPosition, b: CellPosition) {
  method create (line 15) | public static create(row: BaseRowModel, field: ViewFieldRec): CellPositi...
  function fromCursor (line 33) | function fromCursor(position: CursorPos, docModel: DocModel): CellPositi...
  function toCursor (line 55) | function toCursor(position: CellPosition, docModel: DocModel): CursorPos {

FILE: app/client/components/CellSelector.ts
  constant ROW (line 9) | const ROW = "row";
  constant COL (line 10) | const COL = "col";
  constant CELL (line 11) | const CELL = "cell";
  constant NONE (line 12) | const NONE = "";
  type ElemType (line 14) | type ElemType = "row" | "col" | "cell" | "";
  type GridView (line 16) | interface GridView extends BaseView {
  class CellSelector (line 21) | class CellSelector extends Disposable {
    method constructor (line 42) | constructor(public readonly view: GridView) {
    method setToCursor (line 53) | public setToCursor(elemType: ElemType = NONE) {
    method containsCell (line 67) | public containsCell(rowIndex: number, colIndex: number): boolean {
    method containsRow (line 71) | public containsRow(rowIndex: number): boolean {
    method containsCol (line 75) | public containsCol(colIndex: number): boolean {
    method isSelected (line 79) | public isSelected(elem: Element, handlerName: ElemType) {
    method isRowSelected (line 101) | public isRowSelected(rowIndex: number): boolean {
    method isColSelected (line 105) | public isColSelected(colIndex: number): boolean {
    method isCellSelected (line 109) | public isCellSelected(rowIndex: number, colIndex: number): boolean {
    method onlyCellSelected (line 113) | public onlyCellSelected(rowIndex: number, colIndex: number): boolean {
    method isCurrentSelectType (line 118) | public isCurrentSelectType(elemType: ElemType): boolean {
    method isCurrentDragType (line 122) | public isCurrentDragType(elemType: ElemType): boolean {
    method colLower (line 126) | public colLower(): number {
    method colUpper (line 130) | public colUpper(): number {
    method rowLower (line 134) | public rowLower(): number {
    method rowUpper (line 138) | public rowUpper(): number {
    method colCount (line 142) | public colCount(): number {
    method rowCount (line 146) | public rowCount(): number {
    method selectArea (line 150) | public selectArea(rowStartIdx: number, colStartIdx: number, rowEndIdx:...
    method _isCurrentType (line 161) | private _isCurrentType(currentType: ElemType, elemType: ElemType): boo...

FILE: app/client/components/ChartView.ts
  constant MAX_SERIES_IN_CHART (line 46) | const MAX_SERIES_IN_CHART = 100;
  constant DONUT_DEFAULT_HOLE_SIZE (line 47) | const DONUT_DEFAULT_HOLE_SIZE = 0.75;
  constant DONUT_DEFAULT_TEXT_SIZE (line 48) | const DONUT_DEFAULT_TEXT_SIZE = 24;
  function isPieLike (line 54) | function isPieLike(chartType: string) {
  function firstFieldIsLabels (line 58) | function firstFieldIsLabels(chartType: string) {
  function isNumericOnly (line 62) | function isNumericOnly(chartType: string) {
  function visibleColType (line 67) | function visibleColType(col: ColumnRec, use: UseCB = unwrap) {
  function isNumericLike (line 74) | function isNumericLike(col: ColumnRec, use: UseCB = unwrap) {
  function isCategoryType (line 79) | function isCategoryType(pureType: string): boolean {
  type RowPropGetter (line 85) | type RowPropGetter = (rowId: number) => Datum;
  type Series (line 88) | interface Series {
  function getSeriesName (line 96) | function getSeriesName(series: Series, haveMultiple: boolean) {
  type Data (line 113) | type Data = Partial<PlotlyPlotData>;
  type PlotData (line 117) | interface PlotData {
  type DataOptions (line 124) | interface DataOptions extends Data {
  type ChartFunc (line 135) | type ChartFunc = (series: Series[], options: ChartOptions, dataOptions?:...
  function dateGetter (line 139) | function dateGetter(getter: RowPropGetter): RowPropGetter {
  constant LIST_TYPES (line 152) | const LIST_TYPES = ["ChoiceList", "RefList"];
  class ChartView (line 157) | class ChartView extends BaseView {
    method _sortSpec (line 167) | private get _sortSpec() { return this.viewSection.activeSortSpec.peek(...
    method constructor (line 169) | constructor(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
    method prepareToPrint (line 207) | public prepareToPrint(onOff: boolean) {
    method onResize (line 211) | public onResize() {
    method onTableLoaded (line 215) | protected onTableLoaded() {
    method buildDom (line 220) | protected buildDom() {
    method _updateView (line 224) | private async _updateView() {
    method _resizeChart (line 321) | private _resizeChart() {
    method _isCompatibleSeries (line 331) | private _isCompatibleSeries(col: ColumnRec) {
    method _getPlotlyLayout (line 335) | private _getPlotlyLayout(options: ChartOptions): Partial<Layout> {
    method _getPlotlyTheme (line 358) | private _getPlotlyTheme(): Partial<Layout> {
  function groupSeries (line 400) | function groupSeries<T extends Datum>(groupColumn: T[], valueSeries: Ser...
  function extractErrorBars (line 438) | function extractErrorBars(series: Series[], options: ChartOptions): Map<...
  class ChartConfig (line 461) | class ChartConfig extends GrainJSDisposable {
    method constructor (line 567) | constructor(private _gristDoc: GristDoc, private _section: ViewSection...
    method _optionsObj (line 572) | private get _optionsObj() { return this._section.optionsObj; }
    method buildDom (line 574) | public buildDom(): DomContents {
    method _setXAxis (line 706) | private async _setXAxis(colId: string) {
    method _setGroupDataColumn (line 759) | private async _setGroupDataColumn(colId: string) {
    method _getColumns (line 806) | private _getColumns(use: UseCB = unwrap) {
    method _getSummarySourceColumns (line 811) | private _getSummarySourceColumns(use: UseCB = unwrap) {
    method _buildField (line 817) | private _buildField(col: IField) {
    method _buildYAxis (line 829) | private _buildYAxis(): DomContents {
    method _isCompatibleSeries (line 851) | private _isCompatibleSeries(col: ColumnRec, use: UseCB = unwrap) {
    method _setAggregation (line 855) | private async _setAggregation(val: boolean) {
    method _doAggregation (line 874) | private async _doAggregation(): Promise<void> {
    method _undoAggregation (line 883) | private async _undoAggregation() {
    method _isSummaryTable (line 889) | private _isSummaryTable(use: UseCB = unwrap) {
    method _toggleSummaryTable (line 896) | private async _toggleSummaryTable(): Promise<ChartConfig> {
    method _setGroupByColumns (line 906) | private async _setGroupByColumns(groupByCols: string[]) {
    method _ensureValidLinkingIfAny (line 915) | private _ensureValidLinkingIfAny(pageWidget: IPageWidget) {
    method _getColumnIds (line 924) | private _getColumnIds(colIds: string[]) {
  function cssNumberWithSpinnerRow (line 938) | function cssNumberWithSpinnerRow(label: string, value: Computed<number>,...
  function cssSlideRow (line 986) | function cssSlideRow(label: string, value: Computed<number>, save: (val:...
  function cssCheckboxRow (line 1035) | function cssCheckboxRow(label: string, value: KoSaveableObservable<unkno...
  function cssCheckboxRowObs (line 1039) | function cssCheckboxRowObs(label: string, value: Observable<boolean>, .....
  function basicPlot (line 1046) | function basicPlot(series: Series[], options: ChartOptions, dataOptions:...
  method bar (line 1098) | bar(series: Series[], options: ChartOptions): PlotData {
  method line (line 1109) | line(series: Series[], options: ChartOptions): PlotData {
  method area (line 1118) | area(series: Series[], options: ChartOptions): PlotData {
  method scatter (line 1126) | scatter(series: Series[], options: ChartOptions): PlotData {
  method pie (line 1135) | pie(series: Series[], _options: ChartOptions, dataOptions: DataOptions =...
  method donut (line 1160) | donut(series: Series[], options: ChartOptions, dataOptions: DataOptions ...
  method kaplan_meier (line 1191) | kaplan_meier(series: Series[]): PlotData {
  function trimNonNumericData (line 1216) | function trimNonNumericData(series: Series[]): void {
  function replaceEmptyLabels (line 1231) | function replaceEmptyLabels(values: Datum[]): Datum[] {
  function groupIntoSeries (line 1237) | function groupIntoSeries(categoryList: Datum[], valueList: Datum[]): Ser...
  function kaplanMeierPlot (line 1247) | function kaplanMeierPlot(survivalValues: number[]): { x: number, y: numb...

FILE: app/client/components/ClientScope.ts
  class ClientScope (line 10) | class ClientScope extends dispose.Disposable {
    method create (line 13) | public create() {
    method servePlugin (line 21) | public servePlugin(pluginId: string, rpc: Rpc) {
    method _implementStorage (line 36) | private _implementStorage(): Storage {

FILE: app/client/components/Clipboard.ts
  type PasteObj (line 45) | interface PasteObj {
  type CutCallback (line 53) | type CutCallback = () => DocAction | null;
  class Clipboard (line 55) | class Clipboard extends Disposable {
    method contextMenuCopy (line 57) | contextMenuCopy(this: Clipboard) { this._doContextMenuCopy(); }
    method contextMenuCopyWithHeaders (line 58) | contextMenuCopyWithHeaders(this: Clipboard) { this._doContextMenuCopyW...
    method contextMenuCut (line 59) | contextMenuCut(this: Clipboard) { this._doContextMenuCut(); }
    method contextMenuPaste (line 60) | contextMenuPaste(this: Clipboard) { this._doContextMenuPaste().catch(r...
    method allowFocus (line 79) | public static allowFocus(elem: Element): boolean {
    method constructor (line 102) | constructor(private _app: App) {
    method _onCopy (line 151) | private _onCopy(event: ClipboardEvent, elem: HTMLTextAreaElement) {
    method _doContextMenuCopy (line 157) | private _doContextMenuCopy() {
    method _doContextMenuCopyWithHeaders (line 162) | private _doContextMenuCopyWithHeaders() {
    method _onCut (line 167) | private _onCut(event: ClipboardEvent, elem: HTMLTextAreaElement) {
    method _doContextMenuCut (line 173) | private _doContextMenuCut() {
    method _setCBdata (line 178) | private _setCBdata(pasteObj: PasteObj, clipboardData: DataTransfer) {
    method _copyToClipboard (line 189) | private async _copyToClipboard(pasteObj: PasteObj, action: "cut" | "co...
    method _setCutCallback (line 222) | private _setCutCallback(pasteObj: PasteObj, cutData: string) {
    method _onPaste (line 236) | private _onPaste(event: ClipboardEvent, elem: HTMLTextAreaElement) {
    method _doContextMenuPaste (line 250) | private async _doContextMenuPaste() {
    method _doPaste (line 265) | private _doPaste(pasteData: PasteData, plainText: string) {
  constant FOCUS_TARGET_TAGS (line 281) | const FOCUS_TARGET_TAGS = new Set([
  function getPasteData (line 295) | function getPasteData(plainText: string, htmlText: string, fileItems: Fi...
  function getTextFromClipboardItem (line 316) | async function getTextFromClipboardItem(clipboardItem: ClipboardItem | u...
  function getFilesFromClipboardItems (line 330) | async function getFilesFromClipboardItems(clipboardItems: ClipboardItem[...
  function showUnavailableMenuCommandModal (line 343) | function showUnavailableMenuCommandModal(action: "cut" | "copy" | "paste...

FILE: app/client/components/CodeEditorPanel.ts
  class CodeEditorPanel (line 17) | class CodeEditorPanel extends DisposableWithEvents {
    method constructor (line 20) | constructor(private _gristDoc: GristDoc) {
    method buildDom (line 26) | public buildDom() {
    method _onSchemaUpdateAction (line 53) | private async _onSchemaUpdateAction() {

FILE: app/client/components/ColumnTransform.ts
  type AceEditor (line 24) | type AceEditor = any;
  class ColumnTransform (line 30) | class ColumnTransform extends Disposable {
    method constructor (line 54) | constructor(protected gristDoc: GristDoc, private _fieldBuilder: Field...
    method buildDom (line 80) | public buildDom() {
    method finalize (line 84) | public async finalize(): Promise<void> {
    method buildEditorDom (line 92) | protected buildEditorDom(optInit?: string) {
    method prepare (line 118) | public async prepare(optColType?: string) {
    method _doPrepare (line 138) | private async _doPrepare(colType: string) {
    method _shouldIncludeInBundle (line 154) | private _shouldIncludeInBundle(actions: UserAction[]) {
    method addTransformColumn (line 178) | protected async addTransformColumn(colType: string): Promise<number> {
    method postAddTransformColumn (line 200) | protected postAddTransformColumn(): void {
    method cancel (line 204) | public async cancel(): Promise<void> {
    method execute (line 209) | protected async execute(): Promise<void> {
    method _doFinalize (line 216) | private async _doFinalize(): Promise<void> {
    method executeActions (line 248) | protected executeActions(): UserAction[] {
    method cleanup (line 268) | protected cleanup() {
    method getIdentityFormula (line 272) | protected getIdentityFormula() {
    method _setTransforming (line 276) | protected _setTransforming(bool: boolean) {
    method isFinalizing (line 281) | protected isFinalizing(): boolean {
    method preview (line 285) | protected preview() {
    method previewActions (line 293) | protected previewActions(): UserAction[] {

FILE: app/client/components/Comm.ts
  type CommRequestInFlight (line 40) | interface CommRequestInFlight {
  function isCommResponseError (line 52) | function isCommResponseError(msg: CommResponse | CommResponseError): msg...
  class Comm (line 60) | class Comm extends dispose.Disposable implements GristServerAPI, DocList...
    method create (line 93) | public create(reportError?: (err: Error) => void) {
    method initialize (line 120) | public initialize(docId: string | null): GristWSConnection {
    method listConnections (line 140) | public listConnections(): Map<string | null, string | null> {
    method openDoc (line 150) | public async openDoc(docName: string, options?: OpenDocOptions): Promi...
    method useDocConnection (line 159) | public useDocConnection(docId: string): GristWSConnection {
    method releaseDocConnection (line 172) | public releaseDocConnection(docId: string): void {
    method userActionsCollect (line 190) | public userActionsCollect(optYesNo?: boolean): void {
    method userActionsFetchAndReset (line 197) | public userActionsFetchAndReset(): UserAction[] {
    method addUserActions (line 204) | public addUserActions(actions: UserAction[]) {
    method getDocWorkerUrl (line 214) | public getDocWorkerUrl(docId: string | null): string {
    method hasActiveRequests (line 221) | public hasActiveRequests(): boolean {
    method _makeRequest (line 239) | public async _makeRequest(clientId: string | null, docId: string | null,
    method _connection (line 273) | private _connection(docId: string | null): GristWSConnection {
    method _rejectRequests (line 294) | private _rejectRequests(docId: string | null) {
    method _onServerMessage (line 313) | private _onServerMessage(docId: string | null, message: CommResponseBa...
    method _resendPendingRequest (line 380) | private _resendPendingRequest(reqId: number, r: CommRequestInFlight) {
    method _wrapMethod (line 401) | private _wrapMethod<Name extends keyof GristServerAPI>(name: Name): Gr...
  function reqMatchesConnection (line 408) | function reqMatchesConnection(reqDocId: string | null, connDocId: string...

FILE: app/client/components/CopySelection.ts
  class CopySelection (line 15) | class CopySelection {
    method constructor (line 28) | constructor(tableData: TableData, public readonly rowIds: UIRowId[], p...
    method isCellSelected (line 49) | public isCellSelected(rowId: UIRowId, colId: string): boolean {
    method onlyAddRowSelected (line 53) | public onlyAddRowSelected(): boolean {

FILE: app/client/components/CoreBanners.ts
  function buildHomeBanners (line 8) | function buildHomeBanners(app: AppModel) {
  function buildDocumentBanners (line 12) | function buildDocumentBanners(docPageModel: DocPageModel) {

FILE: app/client/components/Cursor.ts
  function nullAsUndefined (line 15) | function nullAsUndefined<T>(value: T | null | undefined): T | undefined {
  type SequenceNum (line 24) | type SequenceNum = number;
  function nextSequenceNum (line 27) | function nextSequenceNum() { // First call to this func should return 1
  class Cursor (line 45) | class Cursor extends Disposable {
    method cursorUp (line 52) | cursorUp(this: Cursor) { this.rowIndex(this.rowIndex()! - 1); }
    method cursorDown (line 53) | cursorDown(this: Cursor) { this.rowIndex(this.rowIndex()! + 1); }
    method cursorLeft (line 54) | cursorLeft(this: Cursor) { this.fieldIndex(this.fieldIndex() - 1); }
    method cursorRight (line 55) | cursorRight(this: Cursor) { this.fieldIndex(this.fieldIndex() + 1); }
    method skipUp (line 56) | skipUp(this: Cursor) { this.rowIndex(this.rowIndex()! - 5); }
    method skipDown (line 57) | skipDown(this: Cursor) { this.rowIndex(this.rowIndex()! + 5); }
    method pageUp (line 58) | pageUp(this: Cursor) { this.rowIndex(this.rowIndex()! - 20); }
    method pageDown (line 59) | pageDown(this: Cursor) { this.rowIndex(this.rowIndex()! + 20); }
    method prevField (line 60) | prevField(this: Cursor) { this.fieldIndex(this.fieldIndex() - 1); }
    method nextField (line 61) | nextField(this: Cursor) { this.fieldIndex(this.fieldIndex() + 1); }
    method moveToFirstRecord (line 62) | moveToFirstRecord(this: Cursor) { this.rowIndex(0); }
    method moveToLastRecord (line 63) | moveToLastRecord(this: Cursor) { this.rowIndex(Infinity); }
    method moveToFirstField (line 64) | moveToFirstField(this: Cursor) { this.fieldIndex(0); }
    method moveToLastField (line 65) | moveToLastField(this: Cursor) { this.fieldIndex(Infinity); }
    method constructor (line 93) | constructor(baseView: BaseView, optCursorPos?: CursorPos) {
    method getCursorPos (line 153) | public getCursorPos(): CursorPos {
    method setCursorPos (line 173) | public setCursorPos(cursorPos: CursorPos, isFromLink: boolean = false,...
    method setLive (line 216) | public setLive(isLive: boolean): void {
    method _cursorEdited (line 224) | private _cursorEdited(): void {
    method _getNewRowIndexForCursorPos (line 237) | private _getNewRowIndexForCursorPos(cursorPos: CursorPos): number | nu...

FILE: app/client/components/CursorMonitor.ts
  type ViewCursorPos (line 12) | type ViewCursorPos = CursorPos & { viewId: ViewDocPage };
  class CursorMonitor (line 19) | class CursorMonitor extends Disposable {
    method constructor (line 29) | constructor(
    method clear (line 51) | public clear() {
    method _whenCursorHasChangedStoreInMemory (line 55) | private _whenCursorHasChangedStoreInMemory(doc: GristDoc) {
    method _whenDocumentLoadsRestorePosition (line 71) | private _whenDocumentLoadsRestorePosition(doc: GristDoc) {
    method _doRestorePosition (line 90) | private async _doRestorePosition(doc: GristDoc) {
    method _abortRestore (line 111) | private _abortRestore() {
    method _storePosition (line 116) | private _storePosition(pos: ViewCursorPos) {
    method _readPosition (line 120) | private _readPosition(view: IDocPage) {
  class StorageWrapper (line 131) | class StorageWrapper {
    method constructor (line 132) | constructor(private _storage = getStorage()) {
    method update (line 136) | public update(docId: string, position: ViewCursorPos): void {
    method clear (line 146) | public clear(docId: string): void {
    method read (line 151) | public read(docId: string): { docId: string; position: ViewCursorPos; ...
    method _key (line 158) | protected _key(docId: string) {
  function oneTimeListener (line 163) | function oneTimeListener<T>(obs: Observable<T>, handler: (value: T) => a...

FILE: app/client/components/CustomCalendarView.ts
  class CustomCalendarView (line 4) | class CustomCalendarView extends CustomView {
    method getBuiltInSettings (line 5) | protected getBuiltInSettings(): CustomViewSettings {

FILE: app/client/components/CustomView.ts
  type CustomViewSettings (line 43) | interface CustomViewSettings {
  class CustomView (line 56) | class CustomView extends BaseView {
    method viewAsCard (line 59) | async viewAsCard(event: Event) {
    method openWidgetConfiguration (line 74) | async openWidgetConfiguration(this: CustomView) {
    method viewAsCard (line 87) | async viewAsCard(event: Event) {
    method constructor (line 114) | constructor(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
    method triggerPrint (line 166) | public async triggerPrint() {
    method getBuiltInSettings (line 172) | protected getBuiltInSettings(): CustomViewSettings {
    method _updatePluginInstance (line 180) | private _updatePluginInstance() {
    method _updateCustomSection (line 197) | private _updateCustomSection() {
    method _buildDom (line 212) | private _buildDom(): HTMLElement {
    method _promptAccess (line 291) | private _promptAccess(access: AccessLevel) {
    method _buildIFrame (line 298) | private _buildIFrame(options: {
  function buildNotification (line 380) | function buildNotification(...args: any[]) {
  function onFrameFocus (line 393) | function onFrameFocus(frame: HTMLIFrameElement, handler: () => void) {

FILE: app/client/components/DataTables.ts
  constant DATA_TABLES_TOOLTIP_KEY (line 25) | const DATA_TABLES_TOOLTIP_KEY = "dataTablesTooltip";
  class DataTables (line 27) | class DataTables extends Disposable {
    method constructor (line 39) | constructor(private _gristDoc: GristDoc) {
    method buildDom (line 49) | public buildDom() {
    method _tableTitle (line 135) | private _tableTitle(table: TableRec, isEditing: Observable<boolean>) {
    method _menuItems (line 165) | private _menuItems(table: TableRec, isEditingName: Observable<boolean>) {
    method _duplicateTable (line 221) | private _duplicateTable(r: TableRec) {
    method _removeTable (line 228) | private _removeTable(r: TableRec) {
    method _editRecordCard (line 239) | private _editRecordCard(r: TableRec) {
    method _enableRecordCard (line 249) | private async _enableRecordCard(r: TableRec) {
    method _disableRecordCard (line 253) | private async _disableRecordCard(r: TableRec) {
    method _tableRows (line 257) | private _tableRows(table: TableRec) {

FILE: app/client/components/DetailView.ts
  class DetailView (line 41) | class DetailView extends BaseView {
    method constructor (line 54) | constructor(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
    method onTableLoaded (line 164) | protected onTableLoaded() {
    method _updateFloatingRow (line 174) | protected _updateFloatingRow() {
    method domToRowModel (line 220) | public domToRowModel(elem: Element, elemType: ElemType): DataRowModel ...
    method domToColModel (line 221) | public domToColModel(elem: Element, elemType: ElemType): DataRowModel ...
    method selectedRows (line 223) | protected selectedRows() {
    method deleteRows (line 230) | protected async deleteRows(rowIds: number[]) {
    method paste (line 250) | protected async paste(data: PasteData, cutCallback: CutCallback | null) {
    method buildCardContextMenu (line 277) | protected buildCardContextMenu(row: DataRowModel) {
    method buildFieldContextMenu (line 282) | protected buildFieldContextMenu() {
    method buildFieldDom (line 293) | protected buildFieldDom(field: RecordLayout.NewField | ViewFieldRec, r...
    method buildDom (line 345) | protected buildDom() {
    method buildTitleControls (line 399) | public override buildTitleControls() {
    method onNewRecordRequest (line 453) | public override onNewRecordRequest() {
    method onResize (line 458) | public override onResize() {
    method onRowResize (line 465) | public override onRowResize(rowModels: BaseRowModel[]): void {
    method makeRecord (line 472) | protected makeRecord(record: DataRowModel) {
    method getRenderedRowModel (line 494) | protected override getRenderedRowModel(rowId: UIRowId) {
    method _isAddRow (line 506) | protected _isAddRow(index: number | null = this.cursor.rowIndex()) {
    method scrollToCursor (line 510) | public async scrollToCursor(sync: boolean = true): Promise<void> {
    method _duplicateRows (line 515) | protected async _duplicateRows(): Promise<number[] | undefined> {
    method _canSingleClick (line 524) | protected _canSingleClick(field: ViewFieldRec) {
    method _clearCardFields (line 541) | protected _clearCardFields() {
    method _hideCardFields (line 558) | protected _hideCardFields() {
    method _clearSelection (line 568) | protected _clearSelection() {
    method _clearCopySelection (line 573) | protected _clearCopySelection() {
    method _getCardContextMenuOptions (line 577) | protected _getCardContextMenuOptions(row: DataRowModel) {
    method _getFieldContextMenuOptions (line 594) | protected _getFieldContextMenuOptions() {

FILE: app/client/components/DocComm.ts
  constant SLOW_NOTIFICATION_TIMEOUT_MS (line 13) | const SLOW_NOTIFICATION_TIMEOUT_MS = 1000;
  class DocComm (line 19) | class DocComm extends Disposable implements ActiveDocAPI {
    method constructor (line 69) | constructor(private _comm: Comm, openResponse: OpenLocalDocResult, pri...
    method getUrlParams (line 91) | public getUrlParams(): { clientId: string, docFD: number } {
    method docUrl (line 97) | public docUrl(path: string) {
    method docWorkerUrl (line 103) | public get docWorkerUrl() {
    method isActionFromThisDoc (line 108) | public isActionFromThisDoc(message: CommMessage): boolean {
    method applyUserActions (line 115) | public applyUserActions(actions: UserAction[], options?: ApplyUAOption...
    method closeDoc (line 124) | public closeDoc(): Promise<void> {
    method forkAndUpdateUrl (line 133) | public async forkAndUpdateUrl(): Promise<void> {
    method _shutdown (line 138) | private async _shutdown() {
    method _setOpenResponse (line 160) | private _setOpenResponse(openResponse: OpenLocalDocResult) {
    method _wrapMethod (line 166) | private _wrapMethod<Name extends keyof ActiveDocAPI>(name: Name): Acti...
    method _callMethod (line 170) | private async _callMethod(name: keyof ActiveDocAPI, ...args: any[]): P...
    method _doCallMethod (line 174) | private async _doCallMethod(name: keyof ActiveDocAPI, ...args: any[]):...
    method _callDocMethod (line 194) | private _callDocMethod(name: keyof ActiveDocAPI, ...args: any[]): Prom...
    method _doForkDoc (line 198) | private async _doForkDoc(): Promise<void> {

FILE: app/client/components/DocumentUsage.ts
  constant DEFAULT_MAX_ROWS (line 30) | const DEFAULT_MAX_ROWS = deploymentType == "saas" ? 20000 : 150000;
  constant DEFAULT_MAX_DATA_SIZE (line 34) | const DEFAULT_MAX_DATA_SIZE = DEFAULT_MAX_ROWS * 2 * 1024;
  constant DEFAULT_MAX_ATTACHMENTS_SIZE (line 37) | const DEFAULT_MAX_ATTACHMENTS_SIZE = 1 * 1024 * 1024 * 1024;
  class DocumentUsage (line 42) | class DocumentUsage extends Disposable {
    method constructor (line 139) | constructor(private _docPageModel: DocPageModel) {
    method buildDom (line 143) | public buildDom() {
    method _buildMessage (line 155) | private _buildMessage() {
    method _buildMetrics (line 193) | private _buildMetrics() {
  function buildLimitStatusMessage (line 211) | function buildLimitStatusMessage(
  function buildUpgradeMessage (line 255) | function buildUpgradeMessage(
  function buildUpgradeLink (line 273) | function buildUpgradeLink(linkText: string, onClick: () => void) {
  function buildRawDataPageLink (line 277) | function buildRawDataPageLink(linkText: string) {
  type MetricOptions (line 281) | interface MetricOptions {
  function buildUsageMetric (line 300) | function buildUsageMetric(options: MetricOptions, ...domArgs: DomElement...
  function buildUsageProgressBar (line 313) | function buildUsageProgressBar(options: MetricOptions) {
  function buildMessage (line 352) | function buildMessage(message: DomContents) {

FILE: app/client/components/Drafts.ts
  class Drafts (line 28) | class Drafts extends Disposable {
    method constructor (line 29) | constructor(
  type Cursor (line 143) | interface Cursor {
  type Editor (line 150) | interface Editor {
  type Notification (line 168) | interface Notification {
  type Storage (line 183) | interface Storage {
  type Tooltip (line 198) | interface Tooltip {
  type State (line 213) | interface State {
  type StateChanged (line 223) | interface StateChanged extends State {
  class CursorAdapter (line 231) | class CursorAdapter extends Disposable implements Cursor {
    method constructor (line 232) | constructor(private _doc: GristDoc) {
    method goToCell (line 236) | public async goToCell(pos: CellPosition): Promise<void> {
  class StorageAdapter (line 241) | class StorageAdapter extends Disposable implements Storage {
    method get (line 243) | public get(): State | null {
    method save (line 247) | public save(ev: State) {
    method hasDraftFor (line 251) | public hasDraftFor(position: CellPosition): boolean {
    method clear (line 259) | public clear(): void {
  function showUndoDiscardNotification (line 267) | function showUndoDiscardNotification(doc: GristDoc, onClick: () => void) {
  class NotificationAdapter (line 280) | class NotificationAdapter extends Disposable implements Notification {
    method constructor (line 286) | constructor(private _doc: GristDoc) {
    method close (line 292) | public close(): void {
    method showUndoDiscard (line 297) | public showUndoDiscard() {
  class TooltipAdapter (line 312) | class TooltipAdapter extends Disposable implements Tooltip {
    method constructor (line 319) | constructor(private _doc: GristDoc) {
    method scheduleClose (line 329) | public scheduleClose(): void {
    method showContinueDraft (line 338) | public showContinueDraft(): void {
    method close (line 354) | public close(): void {
  class EditorAdapter (line 361) | class EditorAdapter extends Disposable implements Editor {
    method constructor (line 369) | constructor(private _doc: GristDoc) {
    method setState (line 413) | public setState(state: any): void {
    method activate (line 418) | public async activate() {
  function cellTooltip (line 438) | function cellTooltip(clb: () => any) {
  function discardNotification (line 458) | function discardNotification(...args: IDomArgs<TagElem<"div">>) {
  function makeWhen (line 470) | function makeWhen(owner: IDisposableOwner) {
  type TypedEmitter (line 477) | interface TypedEmitter<T> {
  type Signal (line 481) | interface Signal {
  type EmitterType (line 485) | type EmitterType<T> = T extends TypedEmitter<infer E> ? TypedEmitter<E> ...
  type EmitterHandler (line 486) | type EmitterHandler<T> = T extends TypedEmitter<infer E> ? ((e: E) => an...

FILE: app/client/components/DropdownConditionConfig.ts
  class DropdownConditionConfig (line 24) | class DropdownConditionConfig extends Disposable {
    method constructor (line 106) | constructor(private _field: ViewFieldRec, private _gristDoc: GristDoc) {
    method buildDom (line 114) | public buildDom() {
    method _getAutocompleteSuggestions (line 172) | private _getAutocompleteSuggestions(): ISuggestionWithValue[] {
  function getUserCompletions (line 196) | function getUserCompletions(user: UserInfo) {

FILE: app/client/components/DropdownConditionEditor.ts
  type BuildDropdownConditionEditorOptions (line 25) | interface BuildDropdownConditionEditorOptions {
  function buildDropdownConditionEditor (line 46) | function buildDropdownConditionEditor(
  function openDropdownConditionEditor (line 70) | function openDropdownConditionEditor(owner: IDisposableOwner, options: {
  type DropdownConditionEditorOptions (line 105) | interface DropdownConditionEditorOptions {
  class DropdownConditionEditor (line 112) | class DropdownConditionEditor extends Disposable {
    method constructor (line 119) | constructor(private _options: DropdownConditionEditorOptions) {
    method attach (line 155) | public attach(cellElem: Element): void {
    method getValue (line 167) | public getValue(): string {
    method blur (line 171) | public blur() {
    method _updateEditorPlaceholder (line 175) | private _updateEditorPlaceholder() {
    method _calcSize (line 190) | private _calcSize(elem: HTMLElement, desiredElemSize: ISize) {

FILE: app/client/components/EditorMonitor.ts
  class EditorMonitor (line 15) | class EditorMonitor extends Disposable {
    method constructor (line 20) | constructor(
    method monitorEditor (line 46) | public monitorEditor(editor: FieldEditor) {
    method _listenToReload (line 67) | private async _listenToReload(doc: GristDoc) {
    method _doRestorePosition (line 85) | private async _doRestorePosition(doc: GristDoc) {
  function typedListener (line 109) | function typedListener(owner: IDisposableOwner) {
  type EditorState (line 116) | type EditorState = any;
  type LastEditData (line 119) | interface LastEditData {
  class EditMemoryStorage (line 127) | class EditMemoryStorage {
    method constructor (line 131) | constructor(private _key: string, private _storage = getStorage()) {
    method updateValue (line 134) | public updateValue(pos: CellPosition, value: EditorState): void {
    method readValue (line 139) | public readValue(): LastEditData | null {
    method clear (line 144) | public clear(): void {
    method timestamp (line 149) | public timestamp(): number {
    method _storageKey (line 153) | protected _storageKey() {
    method load (line 157) | protected load() {
    method save (line 178) | protected save(): void {

FILE: app/client/components/ExternalAttachmentBanner.ts
  type ShowExternalAttachmentBannerPrefer (line 14) | interface ShowExternalAttachmentBannerPrefer {
  class ExternalAttachmentBanner (line 19) | class ExternalAttachmentBanner extends Disposable {
    method constructor (line 23) | constructor(private _docPageModel: DocPageModel) {
    method buildDom (line 39) | public buildDom() {
  function getExternalStorageRecommendation (line 77) | function getExternalStorageRecommendation() {

FILE: app/client/components/FormRenderer.ts
  type FormLayoutNode (line 36) | interface FormLayoutNode {
  type FormLayoutNodeType (line 55) | type FormLayoutNodeType =
  type FormRendererContext (line 70) | interface FormRendererContext {
  function cleanFormLayoutSpec (line 86) | function cleanFormLayoutSpec(
  method new (line 118) | public static new(
  method constructor (line 129) | constructor(
  method reset (line 144) | public reset() {
  class LabelRenderer (line 149) | class LabelRenderer extends FormRenderer {
    method render (line 150) | public render() {
  class ParagraphRenderer (line 155) | class ParagraphRenderer extends FormRenderer {
    method render (line 156) | public render() {
  class SectionRenderer (line 164) | class SectionRenderer extends FormRenderer {
    method render (line 165) | public render() {
  class ColumnsRenderer (line 172) | class ColumnsRenderer extends FormRenderer {
    method render (line 173) | public render() {
    method _getColumnsCount (line 180) | private _getColumnsCount() {
  class SubmitRenderer (line 185) | class SubmitRenderer extends FormRenderer {
    method render (line 186) | public render() {
  class PlaceholderRenderer (line 228) | class PlaceholderRenderer extends FormRenderer {
    method render (line 229) | public render() {
  class LayoutRenderer (line 234) | class LayoutRenderer extends FormRenderer {
    method render (line 235) | public render() {
  class FieldRenderer (line 240) | class FieldRenderer extends FormRenderer {
    method constructor (line 243) | public constructor(layoutNode: FormLayoutNode, context: FormRendererCo...
    method render (line 252) | public render() {
    method reset (line 256) | public reset() {
  method constructor (line 262) | public constructor(protected field: FormField, protected context: FormRe...
  method render (line 266) | public render() {
  method name (line 275) | public name() {
  method id (line 279) | public id() {
  method label (line 283) | public label(): HTMLElement {
  method fieldDomAttributes (line 299) | public fieldDomAttributes(): IAttrObj {
  method getInitialValue (line 303) | protected getInitialValue(): string | null {
  method getInitialValueList (line 312) | protected getInitialValueList(): string[] {
  class BaseTextRenderer (line 322) | class BaseTextRenderer extends BaseFieldRenderer {
    method input (line 329) | public input() {
    method resetInput (line 340) | public resetInput(): void {
    method _renderSingleLineInput (line 344) | private _renderSingleLineInput() {
    method _renderMultiLineInput (line 357) | private _renderMultiLineInput() {
  class TextRenderer (line 370) | class TextRenderer extends BaseTextRenderer {
    method label (line 373) | public label() {
    method input (line 389) | public input(): HTMLTextAreaElement | HTMLInputElement {
  class NumericRenderer (line 406) | class NumericRenderer extends BaseFieldRenderer {
    method input (line 413) | public input() {
    method resetInput (line 421) | public resetInput(): void {
    method getInitialNumericValue (line 426) | protected getInitialNumericValue(): number | "" {
    method _renderTextInput (line 431) | private _renderTextInput() {
    method _renderSpinnerInput (line 444) | private _renderSpinnerInput() {
  class DateRenderer (line 462) | class DateRenderer extends BaseTextRenderer {
  class DateTimeRenderer (line 466) | class DateTimeRenderer extends BaseTextRenderer {
  class ChoiceRenderer (line 472) | class ChoiceRenderer extends BaseFieldRenderer  {
    method constructor (line 485) | public constructor(field: FormField, context: FormRendererContext) {
    method fieldDomAttributes (line 505) | public fieldDomAttributes() {
    method input (line 515) | public input() {
    method resetInput (line 523) | public resetInput() {
    method getInitialValue (line 531) | protected override getInitialValue() {
    method _renderSelectInput (line 536) | private _renderSelectInput() {
    method _renderRadioInput (line 595) | private _renderRadioInput() {
    method _maybeOpenSearchSelect (line 620) | private _maybeOpenSearchSelect(ev: KeyboardEvent) {
  class BoolRenderer (line 631) | class BoolRenderer extends BaseFieldRenderer {
    method render (line 637) | public render() {
    method input (line 643) | public input() {
    method resetInput (line 651) | public resetInput(): void {
    method _renderSwitchInput (line 655) | private _renderSwitchInput() {
    method _renderCheckboxInput (line 668) | private _renderCheckboxInput() {
  class ChoiceListRenderer (line 689) | class ChoiceListRenderer extends BaseFieldRenderer  {
    method constructor (line 697) | public constructor(field: FormField, context: FormRendererContext) {
    method fieldDomAttributes (line 715) | public fieldDomAttributes() {
    method input (line 722) | public input() {
    method resetInput (line 747) | public resetInput(): void {
  class RefListRenderer (line 755) | class RefListRenderer extends BaseFieldRenderer {
    method constructor (line 764) | public constructor(field: FormField, context: FormRendererContext) {
    method fieldDomAttributes (line 779) | public fieldDomAttributes() {
    method input (line 786) | public input() {
    method resetInput (line 812) | public resetInput(): void {
  class RefRenderer (line 820) | class RefRenderer extends BaseFieldRenderer {
    method constructor (line 834) | public constructor(field: FormField, context: FormRendererContext) {
    method fieldDomAttributes (line 852) | public fieldDomAttributes() {
    method input (line 862) | public input() {
    method resetInput (line 870) | public resetInput(): void {
    method getInitialValue (line 879) | protected override getInitialValue() {
    method _renderSelectInput (line 885) | private _renderSelectInput() {
    method _renderRadioInput (line 956) | private _renderRadioInput() {
    method _maybeOpenSearchSelect (line 981) | private _maybeOpenSearchSelect(ev: KeyboardEvent) {
  class AttachmentsRenderer (line 992) | class AttachmentsRenderer extends BaseFieldRenderer {
    method input (line 998) | public input() {
    method resetInput (line 1013) | public resetInput(): void {
  function preventSubmitOnEnter (line 1046) | function preventSubmitOnEnter() {
  function validateRequiredLists (line 1061) | function validateRequiredLists() {
  function sortChoicesInPlace (line 1083) | function sortChoicesInPlace<T>(

FILE: app/client/components/Forms/Columns.ts
  class ColumnsModel (line 20) | class ColumnsModel extends BoxModel {
    method removeChild (line 23) | public removeChild(box: BoxModel) {
    method accept (line 36) | public accept(dropped: FormLayoutNode): BoxModel {
    method render (line 52) | public render(...args: IDomArgs<HTMLElement>): HTMLElement {
    method deleteSelf (line 102) | public async deleteSelf(): Promise<void> {
  class PlaceholderModel (line 128) | class PlaceholderModel extends BoxModel {
    method render (line 129) | public render(...args: IDomArgs<HTMLElement>): HTMLElement {
  function Placeholder (line 238) | function Placeholder(): FormLayoutNode {
  function Columns (line 242) | function Columns(): FormLayoutNode {

FILE: app/client/components/Forms/Editor.ts
  type Props (line 16) | interface Props {
  function buildEditor (line 49) | function buildEditor(props: Props, ...args: IDomArgs<HTMLElement>) {

FILE: app/client/components/Forms/Field.ts
  class FieldModel (line 55) | class FieldModel extends BoxModel {
    method leaf (line 87) | public get leaf() {
    method constructor (line 101) | constructor(box: FormLayoutNode, parent: BoxModel | null, view: FormVi...
    method render (line 122) | public override render(...args: IDomArgs<HTMLElement>): HTMLElement {
    method deleteSelf (line 160) | public async deleteSelf() {
  method constructor (line 180) | constructor(public model: FieldModel) {
  method buildDom (line 184) | public buildDom(props: {
  method renderLabel (line 201) | protected renderLabel(props: {
  class TextModel (line 290) | class TextModel extends Question {
    method renderInput (line 301) | public renderInput() {
    method _renderSingleLineInput (line 314) | private _renderSingleLineInput() {
    method _renderMultiLineInput (line 321) | private _renderMultiLineInput() {
  class NumericModel (line 330) | class NumericModel extends Question {
    method renderInput (line 336) | public renderInput() {
    method _renderTextInput (line 349) | private _renderTextInput() {
    method _renderSpinnerInput (line 356) | private _renderSpinnerInput() {
  class ChoiceModel (line 361) | class ChoiceModel extends Question {
    method constructor (line 379) | constructor(model: FieldModel) {
    method renderInput (line 396) | public renderInput() {
    method _renderSelectInput (line 411) | private _renderSelectInput() {
    method _renderRadioInput (line 427) | private _renderRadioInput() {
  class ChoiceListModel (line 439) | class ChoiceListModel extends ChoiceModel {
    method renderInput (line 444) | public renderInput() {
  class BoolModel (line 461) | class BoolModel extends Question {
    method buildDom (line 467) | public override buildDom(props: {
    method renderInput (line 483) | public override renderInput() {
    method _renderSwitchInput (line 493) | private _renderSwitchInput() {
    method _renderCheckboxInput (line 497) | private _renderCheckboxInput() {
  class DateModel (line 504) | class DateModel extends Question {
    method renderInput (line 505) | public renderInput() {
  class DateTimeModel (line 515) | class DateTimeModel extends Question {
    method renderInput (line 516) | public renderInput() {
  class RefListModel (line 527) | class RefListModel extends Question {
    method constructor (line 540) | constructor(model: FieldModel) {
    method renderInput (line 545) | public renderInput() {
    method _getOptions (line 562) | private _getOptions() {
    method _columnObserver (line 588) | private _columnObserver(
  class RefModel (line 619) | class RefModel extends RefListModel {
    method renderInput (line 625) | public renderInput() {
    method _renderSelectInput (line 643) | private _renderSelectInput() {
    method _renderRadioInput (line 659) | private _renderRadioInput() {
  class AttachmentsModel (line 673) | class AttachmentsModel extends Question {
    method renderInput (line 674) | public renderInput() {
  function fieldConstructor (line 685) | function fieldConstructor(type: string): Constructor<Question> {
  function useFormOptionsLimit (line 702) | function useFormOptionsLimit(use: UseCB, field: ko.Computed<ViewFieldRec...
  function testType (line 709) | function testType(value: BindableValue<string>) {

FILE: app/client/components/Forms/FormConfig.ts
  class FormSelectConfig (line 25) | class FormSelectConfig extends Disposable {
    method constructor (line 26) | constructor(private _field: ViewFieldRec) {
    method buildDom (line 30) | public buildDom() {
  class FormOptionsAlignmentConfig (line 53) | class FormOptionsAlignmentConfig extends Disposable {
    method constructor (line 54) | constructor(private _field: ViewFieldRec) {
    method buildDom (line 58) | public buildDom() {
  class FormOptionsSortConfig (line 80) | class FormOptionsSortConfig extends Disposable {
    method constructor (line 81) | constructor(private _field: ViewFieldRec) {
    method buildDom (line 85) | public buildDom() {
  class FormOptionsLimitConfig (line 108) | class FormOptionsLimitConfig extends Disposable {
    method constructor (line 109) | constructor(private _field: ViewFieldRec) {
    method buildDom (line 113) | public buildDom() {
  function obsPropWithSaveOnWrite (line 147) | function obsPropWithSaveOnWrite<Props extends object, Key extends keyof ...
  class FormFieldRulesConfig (line 159) | class FormFieldRulesConfig extends Disposable {
    method constructor (line 160) | constructor(private _field: ViewFieldRec) {
    method buildDom (line 164) | public buildDom() {

FILE: app/client/components/Forms/FormView.ts
  class FormView (line 40) | class FormView extends BaseView {
    method constructor (line 64) | constructor(gristDoc: GristDoc, viewSectionModel: ViewSectionRec) {
    method insertColumn (line 418) | public insertColumn(colId?: string | null, options?: InsertColOptions) {
    method showColumn (line 422) | public showColumn(colRef: number | string, index?: number) {
    method buildDom (line 426) | public buildDom() {
    method buildOverlay (line 450) | public buildOverlay(...args: IDomArgs) {
    method addNewQuestion (line 456) | public async addNewQuestion(insert: Place, action: { add: string } | {...
    method save (line 483) | public async save() {
    method _handleClickPublish (line 496) | private async _handleClickPublish() {
    method _publishForm (line 532) | private async _publishForm() {
    method _handleClickUnpublish (line 588) | private async _handleClickUnpublish() {
    method _unpublishForm (line 617) | private async _unpublishForm() {
    method _buildPublisher (line 643) | private _buildPublisher() {
    method _getFormUrl (line 739) | private async _getFormUrl() {
    method _buildShareMenu (line 759) | private _buildShareMenu(ctl: IOpenController) {
    method _getSectionCount (line 871) | private _getSectionCount() {
    method _getEstimatedFormHeightPx (line 875) | private _getEstimatedFormHeightPx() {
    method _buildNotifications (line 886) | private _buildNotifications() {
    method _buildFormPublishedNotification (line 892) | private _buildFormPublishedNotification() {
    method _resetForm (line 912) | private async _resetForm() {
  function buildDefaultFormLayout (line 945) | function buildDefaultFormLayout(fields: ViewFieldRec[]): FormLayoutNode {
  constant FORM_TITLE (line 967) | const FORM_TITLE = t("# **Form Title**");
  constant FORM_DESC (line 968) | const FORM_DESC = t("Your form description goes here.");

FILE: app/client/components/Forms/MappedFieldsConfig.ts
  class MappedFieldsConfig (line 22) | class MappedFieldsConfig extends Disposable {
    method constructor (line 23) | constructor(private _section: ViewSectionRec) {
    method buildDom (line 27) | public buildDom() {
    method _buildUnmappedField (line 146) | private _buildUnmappedField(props: { col: ColumnRec, selected: Observa...
    method _buildMappedField (line 179) | private _buildMappedField(props: { col: ColumnRec, selected: Observabl...
  function selectAllLabel (line 205) | function selectAllLabel(...args: any[]) {

FILE: app/client/components/Forms/Menu.ts
  type NewBox (line 18) | type NewBox = { add: string } | { show: string } | { structure: FormLayo...
  type Props (line 20) | interface Props {
  function buildMenu (line 43) | function buildMenu(props: Props, ...args: IDomArgs<HTMLElement>): IDomAr...

FILE: app/client/components/Forms/Model.ts
  type Callback (line 17) | type Callback = () => Promise<void>;
  type Place (line 22) | type Place = (box: FormLayoutNode) => BoxModel;
  method new (line 31) | public static new(box: FormLayoutNode, parent: BoxModel | null, view: Fo...
  method constructor (line 75) | constructor(box: FormLayoutNode, public parent: BoxModel | null, public ...
  method removeChild (line 108) | public removeChild(box: BoxModel) {
  method removeSelf (line 117) | public removeSelf() {
  method deleteSelf (line 126) | public async deleteSelf() {
  method copySelf (line 135) | public async copySelf() {
  method cutSelf (line 144) | public async cutSelf() {
  method willAccept (line 155) | public willAccept(box?: FormLayoutNode | BoxModel | null): "sibling" | "...
  method accept (line 172) | public accept(dropped: FormLayoutNode, hint: "above" | "below" = "above") {
  method prop (line 187) | public prop(name: string, defaultValue?: any) {
  method hasProp (line 194) | public hasProp(name: string) {
  method save (line 198) | public async save(before?: () => MaybePromise<void>): Promise<void> {
  method replaceAtIndex (line 206) | public replaceAtIndex(box: FormLayoutNode, index: number) {
  method swap (line 212) | public swap(box1: BoxModel, box2: BoxModel) {
  method append (line 222) | public append(box: FormLayoutNode) {
  method insert (line 228) | public insert(box: FormLayoutNode, index: number) {
  method replace (line 237) | public replace(existing: BoxModel, newOne: FormLayoutNode | BoxModel) {
  method placeBeforeFirstChild (line 250) | public placeBeforeFirstChild() {
  method placeAfterListChild (line 255) | public placeAfterListChild() {
  method placeAt (line 259) | public placeAt(index: number) {
  method placeAfterChild (line 263) | public placeAfterChild(child: BoxModel) {
  method placeAfterMe (line 267) | public placeAfterMe() {
  method placeBeforeMe (line 271) | public placeBeforeMe() {
  method insertAfter (line 275) | public insertAfter(json: any) {
  method insertBefore (line 279) | public insertBefore(json: any) {
  method root (line 283) | public root() {
  method find (line 292) | public find(droppedId: string | undefined | null): BoxModel | null {
  method filter (line 302) | public* filter(filter: (box: BoxModel) => boolean): Iterable<BoxModel> {
  method includes (line 309) | public includes(box: BoxModel) {
  method kids (line 316) | public kids() {
  method update (line 324) | public update(boxDef: FormLayoutNode) {
  method toJSON (line 371) | public toJSON(): FormLayoutNode {
  method traverse (line 380) | public* traverse(): IterableIterator<BoxModel> {
  method canRemove (line 387) | public canRemove() {
  method onCreate (line 391) | protected onCreate() {
  class LayoutModel (line 396) | class LayoutModel extends BoxModel {
    method constructor (line 399) | constructor(
    method save (line 411) | public async save(clb?: Callback) {
    method render (line 415) | public override render(): HTMLElement {
  class DefaultBoxModel (line 420) | class DefaultBoxModel extends BoxModel {
    method render (line 421) | public render(): HTMLElement {
  function unwrap (line 431) | function unwrap<T>(val: T | Computed<T>): T {
  function parseBox (line 435) | function parseBox(text: string): FormLayoutNode | null {

FILE: app/client/components/Forms/Paragraph.ts
  class ParagraphModel (line 12) | class ParagraphModel extends BoxModel {
    method render (line 20) | public override render(): HTMLElement {
  function Paragraph (line 68) | function Paragraph(text: string, alignment?: "left" | "right" | "center"...

FILE: app/client/components/Forms/Section.ts
  class SectionModel (line 24) | class SectionModel extends BoxModel {
    method constructor (line 25) | constructor(box: FormLayoutNode, parent: BoxModel | null, view: FormVi...
    method render (line 29) | public override render(): HTMLElement {
    method willAccept (line 70) | public override willAccept(): "sibling" | "child" | null {
    method accept (line 78) | public override accept(dropped: FormLayoutNode) {
    method deleteSelf (line 101) | public async deleteSelf(): Promise<void> {
    method canRemove (line 120) | public canRemove() {
  function Section (line 125) | function Section(...children: FormLayoutNode[]): FormLayoutNode {

FILE: app/client/components/Forms/Submit.ts
  class SubmitModel (line 9) | class SubmitModel extends BoxModel {
    method canRemove (line 10) | public canRemove() {
    method render (line 14) | public override render() {

FILE: app/client/components/Forms/elements.ts
  function defaultElement (line 18) | function defaultElement(type: FormLayoutNodeType): FormLayoutNode {

FILE: app/client/components/Forms/styles.ts
  function textbox (line 127) | function textbox(obs: Observable<string | undefined>, ...args: DomElemen...
  constant SHADOW_STYLE (line 468) | const SHADOW_STYLE = `
  function bindMarkdown (line 508) | function bindMarkdown(textObs: BindableValue<string>) {
  function buildMarkdown (line 525) | function buildMarkdown(obs: BindableValue<string>, ...args: IDomArgs<HTM...
  function saveControls (line 789) | function saveControls(editMode: Observable<boolean>, save: (ok: boolean)...

FILE: app/client/components/FormulaTransform.ts
  class FormulaTransform (line 23) | class FormulaTransform extends ColumnTransform {
    method constructor (line 24) | constructor(gristDoc: GristDoc, fieldBuilder: FieldBuilder) {
    method buildDom (line 31) | public buildDom() {

FILE: app/client/components/GridView.ts
  constant SHORT_CLICK_IN_MS (line 79) | const SHORT_CLICK_IN_MS = 500;
  type RowIndexRenderer (line 84) | type RowIndexRenderer = (row: DataRowModel) => DomElementArg | null;
  type CornerRenderer (line 89) | type CornerRenderer = (el: Element) => DomElementArg | null;
  constant PLUS_WIDTH (line 92) | const PLUS_WIDTH = 40;
  constant ROW_NUMBER_WIDTH (line 94) | const ROW_NUMBER_WIDTH = 52;
  type InsertColOptions (line 96) | interface InsertColOptions {
  type Direction (line 103) | type Direction = "left" | "right" | "up" | "down";
  type GridViewOptions (line 105) | interface GridViewOptions extends ViewOptions {
  class GridView (line 122) | class GridView extends BaseView {
    method constructor (line 171) | constructor(gristDoc: GristDoc, viewSectionModel: ViewSectionRec, prot...
    method viewAsCard (line 505) | viewAsCard() {
    method onTableLoaded (line 513) | protected onTableLoaded() {
    method applyAutoSize (line 526) | protected applyAutoSize() {
    method _shiftSelect (line 574) | protected _shiftSelect({ step, direction}: { step: number, direction: ...
    method _shiftSelectUntilFirstOrLastNonEmptyCell (line 609) | protected _shiftSelectUntilFirstOrLastNonEmptyCell({ direction}: { dir...
    method _stepsToContent (line 618) | protected _stepsToContent({ direction}: { direction: Direction }) {
    method _selectionData (line 720) | protected _selectionData(
    method _isCellValueEmpty (line 742) | protected _isCellValueEmpty(value: CellValue | undefined) {
    method paste (line 757) | protected async paste(data: PasteData, cutCallback: CutCallback | null) {
    method _createBulkActionsFromPaste (line 824) | protected _createBulkActionsFromPaste(rowIds: UIRowId[], bulkUpdate: B...
    method getSelection (line 850) | protected getSelection() {
    method clearSelection (line 898) | protected clearSelection() {
    method clearValues (line 908) | protected clearValues(selection: CopySelection) {
    method _clearColumns (line 924) | protected _clearColumns(selection: CopySelection) {
    method _convertFormulasToData (line 932) | protected _convertFormulasToData(selection: CopySelection) {
    method selectAll (line 941) | protected selectAll() {
    method assignCursor (line 958) | protected assignCursor(elem: Element, elemType: ElemType) {
    method scheduleAssignCursor (line 999) | protected scheduleAssignCursor(elem: Element, elemType: ElemType) {
    method preventAssignCursor (line 1009) | protected preventAssignCursor() {
    method selectedRows (line 1014) | protected selectedRows() {
    method deleteRows (line 1019) | protected async deleteRows(rowIds: number[]) {
    method insertColumn (line 1032) | public async insertColumn(colId: string | null = null, options: Insert...
    method makeHeadersFromRow (line 1052) | protected async makeHeadersFromRow(selection: CopySelection) {
    method renameColumn (line 1084) | protected renameColumn(index: number) {
    method scrollPaneBottom (line 1093) | protected scrollPaneBottom() {
    method scrollPaneTop (line 1097) | protected scrollPaneTop() {
    method scrollPaneRight (line 1101) | protected scrollPaneRight() {
    method scrollPaneLeft (line 1105) | protected scrollPaneLeft() {
    method selectColumn (line 1109) | protected selectColumn(colIndex: number) {
    method showColumn (line 1114) | public async showColumn(colRef: number,
    method deleteColumns (line 1122) | protected deleteColumns(selection: CopySelection) {
    method hideFields (line 1142) | protected hideFields(selection: CopySelection) {
    method moveColumns (line 1153) | protected moveColumns(oldIndices: number[], newIndex: number) {
    method moveRows (line 1175) | protected moveRows(oldIndices: number[], newIndex: number) {
    method getMousePosRow (line 1203) | protected getMousePosRow(yCoord: number) {
    method currentMouseRow (line 1213) | protected currentMouseRow(yCoord: number) {
    method getMousePosCol (line 1242) | protected getMousePosCol(mouseX: number) {
    method _getRowStyle (line 1270) | protected _getRowStyle(rowIndex: number) {
    method _getColStyle (line 1274) | protected _getColStyle(colIndex: number) {
    method domToRowModel (line 1281) | public domToRowModel(elem: Element, elemType: ElemType): DataRowModel ...
    method domToColModel (line 1297) | public domToColModel(elem: Element, elemType: ElemType): DataRowModel ...
    method onScroll (line 1317) | protected onScroll() {
    method buildDom (line 1324) | protected buildDom() {
    method onNewRecordRequest (line 1751) | public onNewRecordRequest() {
    method onResize (line 1755) | public override onResize() {
    method onRowResize (line 1774) | public override onRowResize(rowModels: BaseRowModel[]): void {
    method onLinkFilterChange (line 1778) | protected onLinkFilterChange() {
    method _onRenderedVisibleRows (line 1783) | protected _onRenderedVisibleRows(heights?: number[]) {
    method onCellContextMenu (line 1790) | protected onCellContextMenu(ev: Event, elem: Element) {
    method _createColSelectedObs (line 1810) | protected _createColSelectedObs(col: ViewFieldRec) {
    method cellMouseDown (line 1820) | protected cellMouseDown(elem: HTMLElement, event: MouseEvent) {
    method colMouseDown (line 1837) | protected colMouseDown(elem: HTMLElement, event: MouseEvent) {
    method _tooltipMouseDown (line 1849) | protected _tooltipMouseDown(elem: HTMLElement, elemType: ElemType) {
    method rowMouseDown (line 1856) | protected rowMouseDown(elem: HTMLElement, event: MouseEvent) {
    method rowMouseMove (line 1865) | protected rowMouseMove(event: MouseEvent) {
    method colMouseMove (line 1869) | protected colMouseMove(event: MouseEvent) {
    method cellMouseMove (line 1877) | protected cellMouseMove(event: MouseEvent) {
    method createSelector (line 1890) | protected createSelector() {
    method attachSelectorHandlers (line 1897) | protected attachSelectorHandlers() {
    method styleRowDragElements (line 1962) | protected styleRowDragElements(elem: HTMLElement, event: MouseEvent) {
    method styleColDragElements (line 1977) | protected styleColDragElements(elem: HTMLElement, event: MouseEvent) {
    method dragRows (line 2002) | protected dragRows(event: MouseEvent) {
    method dragCols (line 2021) | protected dragCols(event: MouseEvent) {
    method dropRows (line 2050) | protected dropRows() {
    method dropCols (line 2056) | protected dropCols() {
    method columnContextMenu (line 2075) | protected columnContextMenu(
    method _getColumnMenuOptions (line 2094) | protected _getColumnMenuOptions(copySelection: CopySelection): IMultiC...
    method _columnFilterMenu (line 2108) | protected _columnFilterMenu(ctl: IOpenController, field: ViewFieldRec,...
    method maybeSelectColumn (line 2122) | protected maybeSelectColumn(elem: Element, field: ViewFieldRec) {
    method maybeSelectRow (line 2132) | protected maybeSelectRow(elem: Element, rowId: number) {
    method rowContextMenu (line 2141) | protected rowContextMenu() {
    method _getRowContextMenuOptions (line 2146) | protected _getRowContextMenuOptions(): IRowContextMenu {
    method isRecordCardDisabled (line 2160) | public isRecordCardDisabled(): boolean {
    method cellContextMenu (line 2166) | protected cellContextMenu() {
    method _getCellContextMenuOptions (line 2177) | protected _getCellContextMenuOptions(): ICellContextMenu {
    method scrollToCursor (line 2198) | public async scrollToCursor(sync = true) {
    method _duplicateRows (line 2202) | protected async _duplicateRows(): Promise<number[] | undefined> {
    method _clearCopySelection (line 2219) | protected _clearCopySelection() {
    method _showTooltipOnHover (line 2223) | protected _showTooltipOnHover(field: ViewFieldRec, isShowingTooltip: k...
    method _scrollColumnIntoView (line 2237) | protected _scrollColumnIntoView(colIndex: number) {
    method _buildInsertColumnMenu (line 2277) | protected _buildInsertColumnMenu(options: { field?: ViewFieldRec } = {...
    method _openInsertColumnMenu (line 2324) | protected _openInsertColumnMenu(columnIndex: number) {
    method _insertField (line 2334) | protected _insertField(event: KeyboardEvent | undefined, index: number) {
    method _deleteFields (line 2346) | protected _deleteFields() {
    method _applyAutoWidth (line 2360) | protected _applyAutoWidth() {
  function unzipPasteData (line 2401) | function unzipPasteData(pasteData: PasteData): PasteData {
  function growPasteDataMatrix (line 2406) | function growPasteDataMatrix(pasteData: PasteData, r: number, c: number)...
  type ComputedRule (line 2410) | interface ComputedRule {
  function buildStyleOption (line 2414) | function buildStyleOption<Name extends keyof CombinedStyle, T>(
  class HoverColumnTooltip (line 2426) | class HoverColumnTooltip {
    method constructor (line 2428) | constructor(public el: HTMLElement) {
    method show (line 2431) | public show(text: string) {
    method hide (line 2436) | public hide() {
    method dispose (line 2443) | public dispose() {
  function calcZebra (line 2449) | function calcZebra(hex: string) {
  function styleCustomVar (line 2470) | function styleCustomVar(property: string, valueObs: BindableValue<string...
  function scrollBar (line 2474) | function scrollBar(): { width: number; height: number } {

FILE: app/client/components/GristClientSocket.ts
  type GristClientSocketOptions (line 4) | interface GristClientSocketOptions {
  class GristClientSocket (line 8) | class GristClientSocket {
    method constructor (line 23) | constructor(private _url: string, private _options?: GristClientSocket...
    method onmessage (line 27) | public set onmessage(cb: null | ((data: string) => void)) {
    method onopen (line 31) | public set onopen(cb: null | (() => void)) {
    method onerror (line 35) | public set onerror(cb: null | ((err: Error) => void)) {
    method onclose (line 39) | public set onclose(cb: null | (() => void)) {
    method close (line 43) | public close() {
    method send (line 51) | public send(data: string) {
    method pause (line 61) | public pause() {
    method resume (line 65) | public resume() {
    method isOpen (line 69) | public isOpen() {
    method _createWSSocket (line 73) | private _createWSSocket() {
    method _destroyWSSocket (line 91) | private _destroyWSSocket() {
    method _onWSMessage (line 101) | private _onWSMessage(event: WS.MessageEvent | MessageEvent<any>) {
    method _onWSOpen (line 107) | private _onWSOpen() {
    method _onWSError (line 114) | private _onWSError(ev: Event) {
    method _onWSClose (line 128) | private _onWSClose() {
    method _createEIOSocket (line 132) | private _createEIOSocket() {
    method _onEIOMessage (line 148) | private _onEIOMessage(data: string) {
    method _onEIOOpen (line 152) | private _onEIOOpen() {
    method _onEIOError (line 156) | private _onEIOError(err: string | Error) {
    method _onEIOClose (line 160) | private _onEIOClose() {

FILE: app/client/components/GristDoc.ts
  constant RICK_ROLL_YOUTUBE_EMBED_ID (line 122) | const RICK_ROLL_YOUTUBE_EMBED_ID = "dQw4w9WgXcQ";
  type TabContent (line 131) | interface TabContent {
  type TabOptions (line 140) | interface TabOptions {
  type IExtraTool (line 149) | interface IExtraTool {
  type PopupSectionOptions (line 155) | interface PopupSectionOptions {
  type AddSectionOptions (line 161) | interface AddSectionOptions {
  type GristDoc (line 168) | interface GristDoc extends DisposableWithEvents {
  class GristDocImpl (line 242) | class GristDocImpl extends DisposableWithEvents implements GristDoc {
    method regionFocusSwitcher (line 257) | public get regionFocusSwitcher() { return this.app.regionFocusSwitcher; }
    method docApi (line 302) | public get docApi() {
    method constructor (line 351) | constructor(
    method docId (line 828) | public docId() {
    method buildDom (line 835) | public buildDom() {
    method openDocPage (line 914) | public openDocPage(viewId: IDocPage) {
    method setComparison (line 918) | public setComparison(comparison: DocStateComparison | null) {
    method showTool (line 926) | public showTool(tool: typeof RightPanelTool.type): void {
    method onSetCursorPos (line 930) | public async onSetCursorPos(rowModel: BaseRowModel | undefined, fieldM...
    method moveToCursorPos (line 942) | public async moveToCursorPos(cursorPos?: CursorPos, optActionGroup?: M...
    method getUndoStack (line 960) | public getUndoStack() {
    method getActionCounter (line 964) | public getActionCounter() {
    method getTableModel (line 968) | public getTableModel(tableId: string): DataTableModel {
    method getTableModelMaybeWithDiff (line 974) | public getTableModelMaybeWithDiff(tableId: string): DataTableModel {
    method addEmptyTable (line 988) | public async addEmptyTable(): Promise<void> {
    method addWidgetToPage (line 1000) | public async addWidgetToPage(widget: IPageWidget): Promise<void> {
    method addNewPage (line 1026) | public async addNewPage(val: IPageWidget): Promise<void> {
    method saveViewSection (line 1050) | public async saveViewSection(section: ViewSectionRec, newVal: IPageWid...
    method saveLink (line 1097) | public async saveLink(linkId: string, sectionId?: number) {
    method selectBy (line 1122) | public selectBy(widget: IPageWidget) {
    method forkIfNeeded (line 1128) | public async forkIfNeeded() {
    method getCsvLink (line 1134) | public getCsvLink() {
    method getTsvLink (line 1139) | public getTsvLink() {
    method getDsvLink (line 1144) | public getDsvLink() {
    method getXlsxActiveViewLink (line 1149) | public getXlsxActiveViewLink() {
    method recursiveMoveToCursorPos (line 1161) | public async recursiveMoveToCursorPos(
    method activateEditorAtCursor (line 1286) | public async activateEditorAtCursor(options?: { init?: string, state?:...
    method copyAnchorLink (line 1294) | public async copyAnchorLink(anchorInfo: HashLink & CursorPos) {
    method testRenameTable (line 1314) | public async testRenameTable(tableId: string, newTableName: string) {
    method getActionLog (line 1322) | public getActionLog(): ActionLog {
    method _setSectionViewFieldsFromArray (line 1328) | private async _setSectionViewFieldsFromArray(section: ViewSectionRec, ...
    method _onCreateForm (line 1370) | private async _onCreateForm() {
    method _onDocChatter (line 1383) | private _onDocChatter(message: CommDocChatter) {
    method _onDocUsageMessage (line 1409) | private _onDocUsageMessage(message: CommDocUsage) {
    method _onDocUserAction (line 1424) | private _onDocUserAction(message: CommDocUserAction) {
    method _setCursorPos (line 1475) | private async _setCursorPos(cursorPos: CursorPos) {
    method _getCursorPos (line 1513) | private _getCursorPos(): CursorPos {
    method _addWidgetToPage (line 1519) | private async _addWidgetToPage(
    method _addPage (line 1551) | private async _addPage(
    method _focus (line 1594) | private async _focus({ viewRef, sectionRef}: { viewRef?: number, secti...
    method _showNewWidgetPopups (line 1599) | private _showNewWidgetPopups(type: IWidgetType) {
    method _openPopup (line 1610) | private async _openPopup(hash: HashLink) {
    method _playRickRollVideo (line 1691) | private async _playRickRollVideo() {
    method _focusPreviousSection (line 1727) | private _focusPreviousSection() {
    method _waitForView (line 1743) | private async _waitForView(popupSection?: ViewSectionRec) {
    method _getToolContent (line 1776) | private _getToolContent(tool: typeof RightPanelTool.type): IExtraTool ...
    method _maybeShowEditCardLayoutTip (line 1795) | private async _maybeShowEditCardLayoutTip(selectedWidgetType: IWidgetT...
    method _handleNewAttachedCustomWidget (line 1823) | private async _handleNewAttachedCustomWidget(widget: IAttachedCustomWi...
    method _promptForName (line 1838) | private async _promptForName() {
    method _replaceViewSection (line 1846) | private async _replaceViewSection(
    method _onSendActionsStart (line 1906) | private _onSendActionsStart(ev: { cursorPos: CursorPos }) {
    method _onSendActionsEnd (line 1915) | private _onSendActionsEnd(ev: { cursorPos: CursorPos }) {
    method _getDocApiDownloadParams (line 1925) | private _getDocApiDownloadParams() {
    method _switchToSectionId (line 1948) | private async _switchToSectionId(sectionId: number) {
    method _getTableData (line 1969) | private async _getTableData(section: ViewSectionRec): Promise<TableDat...
    method _getCursorPosFromHash (line 1985) | private _getCursorPosFromHash(hash: HashLink): CursorPos {
    method _shouldAutoStartDocTour (line 2006) | private async _shouldAutoStartDocTour(): Promise<boolean> {
    method _shouldAutoStartWelcomeTour (line 2027) | private _shouldAutoStartWelcomeTour(): boolean {
    method _ensureOneNumericSeries (line 2059) | private async _ensureOneNumericSeries(id: number) {
    method _setDefaultFormLayoutSpec (line 2091) | private async _setDefaultFormLayoutSpec(viewSectionId: number) {
    method _handleTriggerQueueOverflowMessage (line 2097) | private _handleTriggerQueueOverflowMessage() {
  function finalizeAnchor (line 2116) | async function finalizeAnchor() {

FILE: app/client/components/GristWSConnection.ts
  constant HEARTBEAT_PERIOD_IN_SECONDS (line 21) | const HEARTBEAT_PERIOD_IN_SECONDS = 45;
  function getDocWorkerUrl (line 26) | async function getDocWorkerUrl(assignmentId: string | null): Promise<str...
  type GristWSSettings (line 39) | interface GristWSSettings {
  class GristWSSettingsBrowser (line 76) | class GristWSSettingsBrowser implements GristWSSettings {
    method makeWebSocket (line 79) | public makeWebSocket(url: string) { return new GristClientSocket(url); }
    method getTimezone (line 80) | public getTimezone()              { return guessTimezone(); }
    method getPageUrl (line 81) | public getPageUrl()               { return G.window.location.href; }
    method getDocWorkerUrl (line 82) | public async getDocWorkerUrl(assignmentId: string | null) {
    method getClientId (line 86) | public getClientId(assignmentId: string | null) {
    method getUserSelector (line 90) | public getUserSelector(): string {
    method updateClientId (line 95) | public updateClientId(assignmentId: string | null, id: string) {
    method advanceCounter (line 99) | public advanceCounter(): string {
    method log (line 105) | public log(...args: any[]): void {
    method warn (line 109) | public warn(...args: any[]): void {
  class GristWSConnection (line 118) | class GristWSConnection extends Disposable {
    method constructor (line 142) | constructor(private _settings: GristWSSettings = new GristWSSettingsBr...
    method initialize (line 148) | public initialize(assignmentId: string | null) {
    method connect (line 174) | public async connect(isReconnecting: boolean = false): Promise<void> {
    method disconnect (line 181) | public disconnect() {
    method established (line 198) | public get established(): boolean {
    method clientId (line 202) | public get clientId(): string | null {
    method docWorkerUrl (line 209) | public get docWorkerUrl(): string {
    method getDocWorkerUrlOrNull (line 217) | public getDocWorkerUrlOrNull(): string | null {
    method onmessage (line 224) | public onmessage(data: string) {
    method send (line 234) | public send(message: any) {
    method _processReceivedMessage (line 244) | private _processReceivedMessage(msgData: string, processClientConnect:...
    method _clearHeartbeat (line 309) | private _clearHeartbeat() {
    method _scheduleHeartbeat (line 317) | private _scheduleHeartbeat() {
    method _sendHeartbeat (line 324) | private _sendHeartbeat() {
    method _connectImpl (line 332) | private _connectImpl(isReconnecting: boolean, timezone: any) {
    method _scheduleReconnect (line 388) | private _scheduleReconnect(isReconnecting: boolean) {
    method _buildWebsocketUrl (line 400) | private _buildWebsocketUrl(isReconnecting: boolean, timezone: any): st...
    method _updateDocWorkerUrl (line 414) | private async _updateDocWorkerUrl() {

FILE: app/client/components/Importer.ts
  type CreatePreviewFunc (line 76) | type CreatePreviewFunc = (vs: ViewSectionRec) => GridView;
  type GridView (line 77) | type GridView = IDisposable & { viewPane: HTMLElement, sortedRows: Sorte...
  constant TABLE_MAPPING (line 78) | const TABLE_MAPPING = 1;
  constant COLUMN_MAPPING (line 79) | const COLUMN_MAPPING = 2;
  type ViewType (line 80) | type ViewType = typeof TABLE_MAPPING | typeof COLUMN_MAPPING;
  type SourceInfo (line 86) | interface SourceInfo {
  function toggleCustomized (line 119) | function toggleCustomized(info: SourceInfo, colId: string, on: boolean):...
  type MergeOptionsStateMap (line 132) | interface MergeOptionsStateMap {
  type MergeOptionsState (line 139) | interface MergeOptionsState {
  function selectAndImport (line 162) | async function selectAndImport(
  function importFromFile (line 200) | async function importFromFile(gristDoc: GristDoc, createPreview: CreateP...
  class Importer (line 236) | class Importer extends DisposableWithEvents {
    method constructor (line 383) | constructor(private _gristDoc: GristDoc,
    method pickAndUploadSource (line 401) | public async pickAndUploadSource(uploadResult: UploadResult | null = n...
    method _setupGlobalEditorCleanup (line 456) | private _setupGlobalEditorCleanup() {
    method _getPrimaryViewSection (line 469) | private _getPrimaryViewSection(tableId: string): ViewSectionRec {
    method _getSectionByRef (line 475) | private _getSectionByRef(sectionRef: number): ViewSectionRec {
    method _updateTransformSection (line 479) | private async _updateTransformSection(sourceInfo: SourceInfo) {
    method _getTransformedDataSource (line 506) | private _getTransformedDataSource(upload: UploadResult): DataSourceTra...
    method _getMergeOptionMaps (line 511) | private _getMergeOptionMaps(upload: UploadResult): MergeOptionsMap[] {
    method _createTransformRuleMap (line 515) | private _createTransformRuleMap(uploadFileIndex: number): TransformRul...
    method _createMergeOptionsMap (line 525) | private _createMergeOptionsMap(uploadFileIndex: number): MergeOptionsM...
    method _createTransformRule (line 535) | private _createTransformRule(sourceInfo: SourceInfo): TransformRule {
    method _getMergeOptionsForSource (line 558) | private _getMergeOptionsForSource(sourceInfo: SourceInfo): MergeOption...
    method _getHiddenTableIds (line 569) | private _getHiddenTableIds(): string[] {
    method _reImport (line 573) | private async _reImport(upload: UploadResult) {
    method _prepareMergeOptions (line 637) | private _prepareMergeOptions() {
    method _maybeFinishImport (line 653) | private async _maybeFinishImport(upload: UploadResult) {
    method _cancelImport (line 679) | private async _cancelImport() {
    method _resetTableMergeOptions (line 690) | private _resetTableMergeOptions(tableId: string) {
    method _validateImportConfiguration (line 694) | private _validateImportConfiguration(): boolean {
    method _buildModalTitle (line 715) | private _buildModalTitle(rightElement?: DomContents) {
    method _buildStaticTitle (line 720) | private _buildStaticTitle() {
    method _updateImportDiff (line 730) | private async _updateImportDiff(info: SourceInfo) {
    method _updateDiff (line 759) | private async _updateDiff(info: SourceInfo) {
    method _resetImportDiffState (line 784) | private _resetImportDiffState() {
    method _cancelPendingDiffRequests (line 794) | private _cancelPendingDiffRequests() {
    method _renderMain (line 802) | private _renderMain(upload: UploadResult) {
    method _makeImportOptionsForCol (line 1175) | private _makeImportOptionsForCol(gristCol: ColumnRec, info: SourceInfo) {
    method _makeImportOptionsMenu (line 1205) | private _makeImportOptionsMenu(transformCol: ColumnRec, others: [strin...
    method _addFocusLayer (line 1219) | private _addFocusLayer(container: HTMLElement) {
    method _setColumnFormula (line 1230) | private async _setColumnFormula(transformCol: ColumnRec, formula: stri...
    method _activateFormulaEditor (line 1247) | private _activateFormulaEditor(refElem: Element, field: ViewFieldRec, ...
    method _setupFormulaEditorCleanup (line 1276) | private _setupFormulaEditorCleanup(
    method _buildSourceSelector (line 1295) | private _buildSourceSelector(owner: MultiHolder, field: ViewFieldRec, ...
    method _buildCustomFormula (line 1361) | private _buildCustomFormula(owner: MultiHolder, field: ViewFieldRec, i...
    method _renderParseOptions (line 1381) | private _renderParseOptions(schema: ParseOptionSchema[], upload: Uploa...
    method _fetchFromDrive (line 1407) | private async _fetchFromDrive(itemUrl: string) {
  class GDriveUrlNotSupported (line 1445) | class GDriveUrlNotSupported extends Error {
    method constructor (line 1446) | constructor(public url: string) {
  class CancelledError (line 1452) | class CancelledError extends Error {
  function getSourceDescription (line 1455) | function getSourceDescription(sourceInfo: SourceInfo, upload: UploadResu...
  function getSourceFileExtension (line 1460) | function getSourceFileExtension(sourceInfo: SourceInfo, upload: UploadRe...

FILE: app/client/components/KeyboardFocusHighlighter.ts
  class KeyboardFocusHighlighter (line 14) | class KeyboardFocusHighlighter extends Disposable {
    method constructor (line 15) | constructor() {

FILE: app/client/components/Layout.ts
  type ContentBox (line 68) | interface ContentBox {
  class LayoutBox (line 80) | class LayoutBox extends Disposable implements ContentBox {
    method create (line 96) | public create(layout: Layout) {
    method getDom (line 145) | public getDom() {
    method maximize (line 149) | public maximize() {
    method buildDom (line 157) | public buildDom() {
    method takeLeafFrom (line 186) | public takeLeafFrom(sourceLayoutBox: ContentBox) {
    method setChildren (line 194) | public setChildren(children: LayoutBox[]) {
    method isFirstChild (line 199) | public isFirstChild() {
    method isLastChild (line 203) | public isLastChild() {
    method isDomDetached (line 209) | public isDomDetached() {
    method getSiblingBox (line 213) | public getSiblingBox(isAfter: boolean) {
    method _addChild (line 226) | public _addChild(childBox: LayoutBox, isAfter: boolean, optNextSibling...
    method addSibling (line 238) | public addSibling(childBox: LayoutBox, isAfter: boolean) {
    method addChild (line 268) | public addChild(childBox: LayoutBox, isAfter: boolean) {
    method toString (line 280) | public toString(): string {
    method _removeChildBox (line 288) | public _removeChildBox(childBox: LayoutBox) {
    method removeFromParent (line 327) | public removeFromParent() {
    method rescaleFlexSizes (line 338) | public rescaleFlexSizes() {
  function makeStatic (line 355) | function makeStatic(valueOrFunc: any) {
  class Layout (line 371) | class Layout extends Disposable {
    method getContainingBox (line 376) | public static getContainingBox(elem: Element | null, optContainer: any) {
    method create (line 394) | public create(boxSpec: BoxSpec, createLeafFunc: (id: string) => HTMLEl...
    method getLeafBox (line 421) | public getLeafBox(leafId: string | number) {
    method getAllLeafIds (line 428) | public getAllLeafIds() {
    method setRoot (line 432) | public setRoot(layoutBox: LayoutBox) {
    method buildDom (line 436) | public buildDom() {
    method forEachBox (line 450) | public forEachBox(cb: (box: LayoutBox) => void, optContext?: any) {
    method buildLayoutBox (line 461) | public buildLayoutBox(boxSpec: BoxSpec) {
    method buildLayout (line 477) | public buildLayout(boxSpec: BoxSpec, needDynamic = false) {
    method _getBoxSpec (line 493) | public _getBoxSpec(layoutBox: LayoutBox) {
    method getLayoutSpec (line 509) | public getLayoutSpec() {
    method getLeafIdMap (line 518) | public getLeafIdMap() {
    method getContainingBox (line 534) | public getContainingBox(elem: Element | null) {

FILE: app/client/components/LayoutEditor.ts
  type JQMouseEvent (line 61) | type JQMouseEvent = JQuery.MouseEventBase | MouseEvent;
  class HelperBox (line 65) | class HelperBox {
    method constructor (line 74) | constructor(data?: Partial<HelperBox>) {
  type TargetPart (line 81) | interface TargetPart {
  type JqueryUI (line 87) | interface JqueryUI {
  type LeafId (line 94) | type LeafId = string | number;
  class Floater (line 100) | class Floater extends Disposable implements ContentBox {
    method create (line 109) | public create(fillWindow?: boolean) {
    method onInitialMouseMove (line 127) | public onInitialMouseMove(mouseEvent: JQMouseEvent, sourceBox: Content...
    method onMouseUp (line 145) | public onMouseUp() {
    method onMouseMove (line 149) | public onMouseMove(mouseEvent: JQMouseEvent) {
  class DropOverlay (line 163) | class DropOverlay extends Disposable {
    method create (line 168) | public create() {
    method detach (line 178) | public detach() {
    method attach (line 187) | public attach(targetElem: HTMLElement) {
    method getAffinity (line 211) | public getAffinity(mouseEvent: JQMouseEvent) {
  class DropTargeter (line 229) | class DropTargeter extends Disposable {
    method create (line 240) | public create(rootElem: HTMLElement) {
    method removeTargetHints (line 250) | public removeTargetHints() {
    method updateTargetHints (line 265) | public updateTargetHints(
    method triggerInsertion (line 369) | public triggerInsertion(part: TargetPart) {
    method accelerateInsertion (line 380) | public accelerateInsertion() {
  class LayoutEditor (line 407) | class LayoutEditor extends Disposable {
    method create (line 429) | public create(layout: Layout) {
    method triggerUserEditStart (line 487) | public triggerUserEditStart() {
    method triggerUserEditStop (line 495) | public triggerUserEditStop() {
    method makeResizable (line 503) | public makeResizable(box: LayoutBox) {
    method unmakeResizable (line 520) | public unmakeResizable(box: LayoutBox) {
    method onResizeStart (line 527) | public onResizeStart(helperObj: HelperBox, isWidth: boolean, event: JQ...
    method onResizeMove (line 541) | public onResizeMove(helperObj: HelperBox, isWidth: boolean, event: JQM...
    method handleMouseDown (line 570) | public handleMouseDown(event: JQMouseEvent, elem: HTMLElement) {
    method dragInNewBox (line 586) | public dragInNewBox(event: JQMouseEvent, leafId: number) {
    method startDragBox (line 595) | public startDragBox(event: JQMouseEvent, box: LayoutBox) {
    method handleMouseUp (line 602) | public handleMouseUp(event: JQMouseEvent) {
    method getBoxFromElement (line 641) | public getBoxFromElement(elem: HTMLElement) {
    method getBox (line 649) | public getBox(leafId: number) {
    method removeContainingBox (line 653) | public removeContainingBox(box: LayoutBox) {
    method doRemoveBox (line 662) | public doRemoveBox(box: ContentBox) {
    method handleMouseMove (line 671) | public handleMouseMove(event: JQMouseEvent) {
    method updateTargets (line 704) | public updateTargets(event: JQMouseEvent) {
    method onInsertBox (line 722) | public async onInsertBox(inserterFunc: (box: LayoutBox) => void) {
  function isAffinityUpDown (line 776) | function isAffinityUpDown(affinity: number): boolean {
  function isAffinityAfter (line 780) | function isAffinityAfter(affinity: number): boolean {
  function getFrac (line 784) | function getFrac(distance: number, max: number): number {
  function round (line 791) | function round(value: number, multipleOf: number) {
  function snap (line 795) | function snap(flexSize: number, sumPrev: number, sumAll: number) {
  function resizeLayoutBox (line 809) | function resizeLayoutBox(layoutBox: LayoutBox, sizeRect: string | DOMRec...
  function rectDesc (line 823) | function rectDesc(rect: string | DOMRect) {
  function resizeLayoutBoxSmoothly (line 832) | function resizeLayoutBoxSmoothly(layoutBox: LayoutBox, startRect: string...
  function adder (line 867) | function adder(sum: number, box: LayoutBox) {

FILE: app/client/components/LayoutTray.ts
  type JQMouseEvent (line 23) | type JQMouseEvent = JQuery.MouseEventBase | MouseEvent;
  class LayoutTray (line 28) | class LayoutTray extends DisposableWithEvents {
    method constructor (line 47) | constructor(public viewLayout: ViewLayout) {
    method replaceLayout (line 106) | public replaceLayout() {
    method buildPopup (line 121) | public buildPopup(owner: IDisposableOwner, selected: Observable<number...
    method buildDom (line 145) | public buildDom() {
    method buildContentDom (line 164) | public buildContentDom(id: string | number) {
    method _registerCommands (line 171) | private _registerCommands() {
  class CollapsedDropZone (line 240) | class CollapsedDropZone extends Disposable {
    method constructor (line 247) | constructor(protected model: LayoutTray) {
    method buildDom (line 265) | public buildDom() {
    method _start (line 318) | private _start() {
    method _stop (line 322) | private _stop() {
    method _isAnimating (line 326) | private _isAnimating() {
    method _calculate (line 330) | private _calculate(parentRect: DOMRect) {
    method _insertDropTarget (line 386) | private async _insertDropTarget(index: number) {
    method _removeDropZone (line 398) | private async _removeDropZone() {
  class CollapsedLayout (line 414) | class CollapsedLayout extends Disposable {
    method constructor (line 428) | constructor(protected model: LayoutTray) {
    method all (line 437) | public all() {
    method buildLayout (line 441) | public buildLayout(leafs: number[]) {
    method addBox (line 449) | public addBox(id: number | Leaf, index?: number) {
    method indexOf (line 458) | public indexOf(box: Leaf) {
    method insert (line 462) | public insert(index: number, leaf: Leaf) {
    method remove (line 475) | public remove(leaf: Leaf) {
    method destroy (line 490) | public destroy(leaf: Leaf) {
    method leafIds (line 494) | public leafIds() {
    method getBox (line 498) | public getBox(leaf: number): CollapsedLeaf | undefined {
    method buildDom (line 502) | public buildDom() {
  type Draggable (line 512) | interface Draggable {
  type Dropped (line 519) | interface Dropped {
  method buildDom (line 530) | public buildDom(): HTMLElement | null {
  class EmptyLeaf (line 538) | class EmptyLeaf extends Leaf {
    method constructor (line 544) | constructor(protected model: LayoutTray) {
    method monitorDrop (line 549) | public monitorDrop() {
    method buildDom (line 566) | public buildDom() {
  class TargetLeaf (line 578) | class TargetLeaf extends EmptyLeaf {
    method buildDom (line 579) | public buildDom() {
    method insert (line 590) | public insert(index: number) {
    method remove (line 605) | public remove() {
  class CollapsedLeaf (line 620) | class CollapsedLeaf extends Leaf implements Draggable, Dropped {
    method constructor (line 643) | constructor(protected model: LayoutTray, id: number) {
    method detach (line 662) | public detach() {
    method attach (line 666) | public attach() {
    method buildDom (line 675) | public buildDom() {
    method dragStart (line 710) | public dragStart(ev: DragEvent, floater: MiniFloater) {
    method dragEnd (line 725) | public dragEnd(ev: DragEvent) {
    method drag (line 729) | public drag(ev: DragEvent) {
    method drop (line 733) | public drop(ev: DragEvent, floater: MiniFloater) {
    method removeFromLayout (line 745) | public removeFromLayout() {
    method leafId (line 751) | public leafId() {
    method _buildHidden (line 755) | private _buildHidden() {
  class MiniFloater (line 766) | class MiniFloater extends Disposable {
    method constructor (line 769) | constructor() {
    method buildDom (line 779) | public buildDom() {
    method onMove (line 787) | public onMove(ev: JQMouseEvent) {
  class ExternalLeaf (line 799) | class ExternalLeaf extends Disposable implements Dropped {
    method constructor (line 808) | constructor(protected model: LayoutTray) {
    method removeFromLayout (line 907) | public removeFromLayout() {
    method leafId (line 919) | public leafId() {
    method _replaceFloater (line 926) | private _replaceFloater() {
  class ArrayHolder (line 984) | class ArrayHolder extends Disposable {
    method constructor (line 987) | constructor() {
    method autoDispose (line 1001) | public autoDispose<T extends IDisposable>(obj: T): T {
    method release (line 1006) | public release(obj: IDisposable) {
  function syncHover (line 1015) | function syncHover(obs: Signal) {
  function detachedNode (line 1023) | function detachedNode(node: Observable<HTMLElement | null>) {
  function findDraggable (line 1035) | function findDraggable(ev: EventTarget | null) {
  function asDraggable (line 1046) | function asDraggable(item: Draggable) {
  function useDragging (line 1059) | function useDragging() {
  class VRect (line 1149) | class VRect {
    method constructor (line 1155) | constructor(offset: DOMRect, params: Partial<VRect>) {
    method contains (line 1163) | public contains(ev: MouseEvent) {

FILE: app/client/components/LinkingState.ts
  type LinkType (line 28) | type LinkType = "Filter:Summary-Group" |
  type FilterState (line 41) | type FilterState = FilterColValues & {
  function FilterStateToColValues (line 45) | function FilterStateToColValues(fs: FilterState) { return pick(fs, ["fil...
  class LinkingState (line 51) | class LinkingState extends Disposable {
    method constructor (line 82) | constructor(docModel: DocModel, linkConfig: LinkConfig) {
    method disableEditing (line 348) | public disableEditing(): boolean {
    method _makeFilterObs (line 376) | private _makeFilterObs(
    method _srcCustomFilter (line 530) | private _srcCustomFilter(
    method _makeValGetter (line 551) | private _makeValGetter(
  function isSummaryOf (line 586) | function isSummaryOf(summary: TableRec, detail: TableRec): boolean {
  function summaryGetCorrespondingCol (line 608) | function summaryGetCorrespondingCol(srcGBCol: ColumnRec, tgtTable: Table...

FILE: app/client/components/ParseOptions.ts
  type ParseOptionValueType (line 13) | type ParseOptionValueType = boolean | string | number;
  type ParseOptionValues (line 17) | interface ParseOptionValues {
  type EscapeChars (line 25) | interface EscapeChars {
  function escapeChars (line 38) | function escapeChars(value: string) {
  function unescapeChars (line 41) | function unescapeChars(value: string) {
  function buildParseOptionsForm (line 50) | function buildParseOptionsForm(
  function optionToInput (line 99) | function optionToInput(owner: IDisposableOwner, type: string, value: Obs...

FILE: app/client/components/PluginScreen.ts
  type RenderOptions (line 16) | interface RenderOptions {
  class PluginScreen (line 25) | class PluginScreen extends Disposable {
    method constructor (line 31) | constructor(private _title: string) {
    method renderContent (line 37) | public renderContent(inlineElement: HTMLElement) {
    method renderPlugin (line 42) | public renderPlugin(plugin: PluginInstance): RenderTarget {
    method render (line 51) | public render(content: DomContents, options?: RenderOptions) {
    method renderError (line 59) | public renderError(message: string) {
    method renderSpinner (line 73) | public renderSpinner() {
    method close (line 78) | public close() {
    method showImportDialog (line 83) | public showImportDialog(options?: IModalOptions) {
    method _buildModalTitle (line 108) | private _buildModalTitle(rightElement?: DomContents) {

FILE: app/client/components/Printing.ts
  type RowId (line 10) | type RowId = number | "new";
  function getViewSectionContent (line 12) | function getViewSectionContent(viewInstance: BaseView | null) {
  function printViewSection (line 32) | async function printViewSection(layout: any, viewSection: ViewSectionRec) {
  function renderAllRows (line 119) | function renderAllRows(

FILE: app/client/components/RawDataPage.ts
  class RawDataPage (line 19) | class RawDataPage extends Disposable {
    method constructor (line 21) | constructor(private _gristDoc: GristDoc) {
    method buildDom (line 47) | public buildDom() {
    method _close (line 69) | private _close() {
  class RawDataPopup (line 74) | class RawDataPopup extends Disposable {
    method constructor (line 75) | constructor(
    method buildDom (line 98) | public buildDom() {

FILE: app/client/components/RecordCardPopup.ts
  type RecordCardPopupOptions (line 15) | interface RecordCardPopupOptions {
  class RecordCardPopup (line 22) | class RecordCardPopup extends DisposableWithEvents {
    method constructor (line 29) | constructor(private _options: RecordCardPopupOptions) {
    method buildDom (line 42) | public buildDom() {
    method _onRowChange (line 64) | private _onRowChange(type: ChangeType, rows: RowList) {

FILE: app/client/components/RecordLayout.js
  function RecordLayout (line 54) | function RecordLayout(options) {
  function processBox (line 225) | function processBox(spec) {

FILE: app/client/components/RecordLayoutEditor.js
  function RecordLayoutEditor (line 27) | function RecordLayoutEditor(recordLayout, layout, optResizeCallback) {

FILE: app/client/components/RefSelect.ts
  type Item (line 19) | interface Item {
  class RefSelect (line 27) | class RefSelect extends Disposable {
    method constructor (line 37) | constructor(options: {
    method buildDom (line 87) | public buildDom() {
    method _addFormulaField (line 122) | private async _addFormulaField(item: Item) {
    method _removeFormulaField (line 166) | private _removeFormulaField(item: Item) {
    method _getReferrerFields (line 187) | private _getReferrerFields(colId: string) {
    method _getReferencedCols (line 197) | private _getReferencedCols(field: ViewFieldRec) {
    method _getFormulaMatchSet (line 206) | private _getFormulaMatchSet(field: ViewFieldRec) {

FILE: app/client/components/RegionFocusSwitcher.ts
  type Panel (line 19) | type Panel = "left" | "top" | "right" | "main";
  type PanelRegion (line 20) | interface PanelRegion {
  type SectionRegion (line 24) | interface SectionRegion {
  type Region (line 28) | type Region = PanelRegion | SectionRegion;
  type StateUpdateInitiator (line 29) | type StateUpdateInitiator = { type: "cycle" } | { type: "mouse", event?:...
  type State (line 30) | interface State {
  class RegionFocusSwitcher (line 40) | class RegionFocusSwitcher extends Disposable {
    method _gristDocObs (line 47) | private get _gristDocObs() { return this._app?.pageModel?.gristDoc; }
    method constructor (line 64) | constructor(private _app?: App) {
    method onPageDomLoaded (line 100) | public onPageDomLoaded(el: HTMLElement) {
    method panelAttrs (line 108) | public panelAttrs(id: Panel, ariaLabel: string) {
    method getRegionId (line 152) | public getRegionId(region?: Region) {
    method focusRegion (line 165) | public focusRegion(id: Panel) {
    method focusActiveSection (line 179) | public focusActiveSection() {
    method addListener (line 190) | public addListener(listener: (regionId: Panel, prevRegionId: Panel) =>...
    method reset (line 201) | public reset() {
    method _focusRegion (line 205) | private _focusRegion(
    method _cycle (line 226) | private _cycle(direction: "next" | "prev") {
    method _onClick (line 242) | private _onClick(event: MouseEvent) {
    method _onEscapeKeypress (line 287) | private _onEscapeKeypress() {
    method _savePrevElementState (line 330) | private _savePrevElementState(prev: Region | undefined) {
    method _onStateChange (line 343) | private _onStateChange(current: State, prev: State) {
    method _toggleCreatorPanel (line 402) | private _toggleCreatorPanel() {
    method _canTabThroughMainRegion (line 415) | private _canTabThroughMainRegion(use: UseCBOwner) {
    method _getGristDoc (line 432) | private _getGristDoc() {
    method _logCommand (line 442) | private _logCommand(name: "nextRegion" | "prevRegion" | "creatorPanel") {
    method _maybeNotifyAboutCreatorPanel (line 455) | private _maybeNotifyAboutCreatorPanel() {
  constant ATTRS (line 509) | const ATTRS = {

FILE: app/client/components/SelectionSummary.ts
  type Range (line 25) | interface Range {
  type SummaryPart (line 33) | interface SummaryPart {
  constant MAX_CELLS_TO_SCAN (line 48) | const MAX_CELLS_TO_SCAN = 1_000_000;
  class SelectionSummary (line 50) | class SelectionSummary extends Disposable {
    method constructor (line 94) | constructor(
    method buildDom (line 115) | public buildDom() {
    method _onSpliceChange (line 129) | private _onSpliceChange(splice: { start: number }) {
    method _onRowNotify (line 143) | private _onRowNotify(rows: RowsChanged) {
    method _scheduleRecalc (line 165) | private _scheduleRecalc() {
    method _recalc (line 170) | private _recalc() {
  function doCopy (line 268) | async function doCopy(value: string, elem: Element) {

FILE: app/client/components/TypeConversion.ts
  type PrepColInfo (line 19) | interface PrepColInfo {
  function addColTypeSuffix (line 33) | function addColTypeSuffix(type: string, column: ColumnRec, docModel: Doc...
  function inferColTypeSuffix (line 52) | function inferColTypeSuffix(newPure: string, column: ColumnRec) {
  function getRefTableIdFromData (line 72) | function getRefTableIdFromData(docModel: DocModel, column: ColumnRec): s...
  function prepTransformColInfo (line 105) | async function prepTransformColInfo(options: {
  function guessWidgetOptionsSync (line 202) | function guessWidgetOptionsSync(options: {
  function setDisplayFormula (line 287) | async function setDisplayFormula(
  function getVisibleColName (line 300) | function getVisibleColName(docModel: DocModel, visibleColRef: number): s...
  function isReferenceCol (line 305) | function isReferenceCol(colModel: ColumnRec) {

FILE: app/client/components/TypeTransform.ts
  class TypeTransform (line 32) | class TypeTransform extends ColumnTransform {
    method constructor (line 38) | constructor(gristDoc: GristDoc, fieldBuilder: FieldBuilder) {
    method buildDom (line 54) | public buildDom() {
    method addTransformColumn (line 107) | protected async addTransformColumn(toType: string) {
    method convertValuesActions (line 140) | protected convertValuesActions(): UserAction[] {
    method convertValues (line 150) | protected async convertValues() {
    method executeActions (line 157) | protected executeActions(): UserAction[] {
    method postAddTransformColumn (line 164) | protected postAddTransformColumn() {
    method cleanup (line 177) | protected cleanup() {
    method setType (line 184) | public async setType(toType: string) {

FILE: app/client/components/UndoStack.ts
  type ActionGroupWithCursorPos (line 11) | interface ActionGroupWithCursorPos extends MinimalActionGroup {
  type IUndoState (line 19) | interface IUndoState {
  class UndoStack (line 28) | class UndoStack extends dispose.Disposable {
    method create (line 42) | public create(log: MinimalActionGroup[], options: {
    method pushAction (line 87) | public pushAction(ag: MinimalActionGroup): void {
    method sendUndoAction (line 117) | public async sendUndoAction(): Promise<void> {
    method sendRedoAction (line 124) | public async sendRedoAction(): Promise<void> {
    method enable (line 130) | public enable(): void {
    method disable (line 134) | public disable(): void {
    method _sendAction (line 138) | private async _sendAction(isUndo: boolean): Promise<void> {
    method _findActionBundle (line 177) | private _findActionBundle(ag: ActionGroupWithCursorPos) {

FILE: app/client/components/UnsavedChanges.ts
  class UnsavedChange (line 11) | class UnsavedChange extends Disposable {
    method constructor (line 12) | constructor(
    method haveUnsavedChanges (line 23) | public haveUnsavedChanges() { return !this._haveChanges || this._haveC...
    method save (line 24) | public async save(): Promise<void> { return this._saveCB?.(); }
  class UnsavedChangeSet (line 27) | class UnsavedChangeSet {
    method haveUnsavedChanges (line 33) | public haveUnsavedChanges(): boolean {
    method saveChanges (line 40) | public async saveChanges(): Promise<void> {
    method add (line 44) | public add(unsaved: UnsavedChange) { this._changes.add(unsaved); }
    method delete (line 45) | public delete(unsaved: UnsavedChange) { this._changes.delete(unsaved); }

FILE: app/client/components/VersionUpdateBanner.ts
  type ShowVersionUpdateBannerPrefer (line 12) | interface ShowVersionUpdateBannerPrefer {
  class VersionUpdateBanner (line 17) | class VersionUpdateBanner extends Disposable {
    method constructor (line 21) | constructor(private _appModel: AppModel) {
    method buildDom (line 35) | public buildDom() {

FILE: app/client/components/ViewAsBanner.ts
  class ViewAsBanner (line 24) | class ViewAsBanner extends Disposable {
    method constructor (line 28) | constructor(private _docPageModel: DocPageModel) {
    method buildDom (line 32) | public buildDom() {
    method _buildContent (line 45) | private _buildContent(userOverride: UserOverride) {
    method _initViewAsUsers (line 87) | private async _initViewAsUsers() {
    method _getUsersForViewAs (line 92) | private _getUsersForViewAs(): Promise<PermissionDataWithExtraUsers> {

FILE: app/client/components/ViewConfigTab.js
  function ViewSectionData (line 27) | function ViewSectionData(section) {
  function ViewConfigTab (line 36) | function ViewConfigTab(options) {

FILE: app/client/components/ViewLayout.ts
  function getInstanceConstructor (line 60) | function getInstanceConstructor(parentKey: string) {
  class ViewSectionHelper (line 69) | class ViewSectionHelper extends Disposable {
    method constructor (line 72) | constructor(gristDoc: GristDoc, vs: ViewSectionRec, options?: any) {
  class ViewLayout (line 98) | class ViewLayout extends DisposableWithEvents implements IDomComponent {
    method constructor (line 112) | constructor(public readonly gristDoc: GristDoc, viewId: number) {
    method buildDom (line 242) | public buildDom() {
    method freezeUntil (line 277) | public async freezeUntil<T>(promise: Promise<T>): Promise<T> {
    method getFullLayoutSpec (line 290) | public getFullLayoutSpec() {
    method saveLayoutSpec (line 296) | public saveLayoutSpec(specs?: BoxSpec) {
    method removeViewSection (line 316) | public async removeViewSection(viewSectionRowId: number) {
    method rebuildLayout (line 376) | public rebuildLayout(layoutSpec: BoxSpec) {
    method _expandSection (line 388) | private _expandSection() {
    method _buildLeafContent (line 396) | private _buildLeafContent(sectionRowId: number) {
    method _updateLayoutSpecWithSections (line 409) | private _updateLayoutSpecWithSections(spec: BoxSpec) {
    method _onResize (line 415) | private _onResize() {
    method _maybeFocusInSection (line 424) | private _maybeFocusInSection()  {
    method _openSortFilterMenu (line 437) | private _openSortFilterMenu(sectionId?: number)  {
  constant DELETE_WIDGET (line 447) | const DELETE_WIDGET = "deleteOnlyWidget";
  constant DELETE_DATA (line 448) | const DELETE_DATA = "deleteDataAndWidget";
  constant CANCEL (line 449) | const CANCEL = "cancel";
  type PromptAction (line 450) | type PromptAction = typeof DELETE_WIDGET | typeof DELETE_DATA | typeof C...
  function widgetRemovalPrompt (line 452) | function widgetRemovalPrompt(tableName: string): Promise<PromptAction> {

FILE: app/client/components/VirtualDoc.ts
  class VirtualDoc (line 75) | class VirtualDoc extends DisposableWithEvents implements GristDoc {
    method constructor (line 114) | constructor(public appModel: AppModel) {
    method focus (line 184) | public focus() {
    method buildDom (line 193) | public buildDom() {
    method tableDef (line 210) | public tableDef(tableId: string) {
    method addTable (line 215) | public addTable(table: TableSpec) {
    method hideColumn (line 350) | public async hideColumn(tableId: string, colId: string) {
    method showColumn (line 372) | public async showColumn(tableId: string, colId: string) {
    method getColumnRec (line 394) | public getColumnRec(tableId: string, colId: string) {
    method getTableRec (line 402) | public getTableRec(tableId: string) {
    method getMainSectionRec (line 407) | public getMainSectionRec(tableId: string) {
    method refreshTableData (line 418) | public async refreshTableData(tableId: string) {
    method setView (line 424) | public setView(label: string) {
    method getRecords (line 433) | public getRecords(table: string) {
    method onSetCursorPos (line 446) | public async onSetCursorPos(rowModel: BaseRowModel | undefined, fieldM...
    method getTableModelMaybeWithDiff (line 460) | public getTableModelMaybeWithDiff(tableId: string) {
    method getTableModel (line 469) | public getTableModel(tableId: string) {
    method docId (line 473) | public docId(): string {
    method openDocPage (line 477) | public async openDocPage(viewId: IDocPage): Promise<void> {
    method showTool (line 481) | public showTool(tool: "none" | "docHistory" | "validations" | "discuss...
    method moveToCursorPos (line 484) | public async moveToCursorPos(cursorPos?: CursorPos, optActionGroup?: M...
    method getUndoStack (line 488) | public getUndoStack(): UndoStack {
    method getActionCounter (line 492) | public getActionCounter(): ActionCounter {
    method addEmptyTable (line 496) | public async addEmptyTable(): Promise<void> {
    method addWidgetToPage (line 500) | public async addWidgetToPage(widget: IPageWidget): Promise<void> {
    method addNewPage (line 504) | public async addNewPage(val: IPageWidget): Promise<void> {
    method saveViewSection (line 508) | public async saveViewSection(section: ViewSectionRec, newVal: IPageWid...
    method saveLink (line 512) | public async saveLink(linkId: string, sectionId?: number): Promise<any> {
    method selectBy (line 516) | public selectBy(widget: IPageWidget): any[] {
    method forkIfNeeded (line 520) | public async forkIfNeeded(): Promise<void> {
    method recursiveMoveToCursorPos (line 524) | public async recursiveMoveToCursorPos(
    method activateEditorAtCursor (line 533) | public async activateEditorAtCursor(options?: { init?: string; state?:...
    method copyAnchorLink (line 537) | public async copyAnchorLink(_anchorInfo: unknown) {}
    method getCsvLink (line 539) | public getCsvLink() {
    method getTsvLink (line 543) | public getTsvLink() {
    method getDsvLink (line 547) | public getDsvLink() {
    method getXlsxActiveViewLink (line 551) | public getXlsxActiveViewLink() {
    method sendTableAction (line 555) | public async sendTableAction() {}
    method sendTableActions (line 556) | public async sendTableActions() {}
    method getActionLog (line 557) | public getActionLog(): ActionLog {
    method setComparison (line 561) | public setComparison(comparison: DocStateComparison | null) {
  type ExternalData (line 569) | interface ExternalData {
  type ExternalFormat (line 576) | interface ExternalFormat {
  type VirtualRowId (line 583) | type VirtualRowId = string | UIRowId;
  class VirtualSection (line 588) | class VirtualSection extends Disposable {
    method constructor (line 593) | constructor(protected _doc: VirtualDoc, protected props: {
    method buildDom (line 761) | public buildDom() {
    method _syncColumns (line 797) | private _syncColumns() {
  class ApiData (line 827) | class ApiData implements ExternalData {
    method constructor (line 828) | constructor(private _fun: () => MaybePromise<any>) {
    method getData (line 831) | public async getData() {
  class RecordsFormat (line 839) | class RecordsFormat implements ExternalFormat {
    method convert (line 840) | public convert(tableId: string, data: TableRecordValues, keys: string[...
  class RawFormat (line 855) | class RawFormat implements ExternalFormat {
    method convert (line 856) | public convert(tableId: string, data: any[], keys: string[]): TableDat...
  type TableSpec (line 870) | interface TableSpec {
  type ColumnSpec (line 887) | interface ColumnSpec<T = string> {
  class InMemoryDocModel (line 913) | class InMemoryDocModel extends DocModel {
    method constructor (line 914) | constructor() {
  class InMemoryApp (line 950) | class InMemoryApp extends DisposableWithEvents implements App {
    method constructor (line 955) | constructor(public topAppModel: TopAppModel) {
  class InMemoryDocPageModel (line 964) | class InMemoryDocPageModel extends DocPageModelImpl {
    method initialize (line 965) | public override initialize(): void {
  function generateInitialActions (line 974) | function generateInitialActions(tabDef: TableSpec): DocAction[] {
  function properId (line 1044) | function properId(label: string) {
  function maybePeek (line 1048) | function maybePeek<T>(value: T | Observable<T>) {
  function maybeUse (line 1052) | function maybeUse<T>(use: UseCB, obs: BaseObservable<T> | T): T {

FILE: app/client/components/VirtualTable.ts
  class VirtualTable (line 51) | class VirtualTable extends Disposable {
    method constructor (line 70) | constructor(options: {
    method rename (line 88) | public rename(name: string) {
    method addColumn (line 95) | public addColumn(...cols: (Partial<ColDef> & { label: string })[]) {
    method setData (line 108) | public setData(args: any) {
    method buildDom (line 117) | public buildDom() {
    method _build (line 125) | private _build() {
    method _transform (line 271) | private _transform(rows: any[]): any {
  class ExternalTable (line 287) | class ExternalTable extends Disposable implements IExternalTable {
    method beforeEdit (line 293) | public async beforeEdit(editor: IEdit) {}
    method afterEdit (line 294) | public async afterEdit(editor: IEdit) {}
    method afterAnySchemaChange (line 295) | public async afterAnySchemaChange(editor: IEdit) {}
    method sync (line 296) | public async sync(editor: IEdit): Promise<void> {}
  function toId (line 300) | function toId(label: string) {
  function toTableData (line 305) | function toTableData(name: string, data: Record<string, any>[]): TableDa...
  class InMemoryGristDoc (line 321) | class InMemoryGristDoc extends Disposable {
    method constructor (line 345) | constructor(public docModel: DocModel, viewId: any) {
    method getCsvLink (line 358) | public getCsvLink() {
    method getTsvLink (line 362) | public getTsvLink() {
    method getDsvLink (line 366) | public getDsvLink() {
    method getXlsxActiveViewLink (line 370) | public getXlsxActiveViewLink() {
    method clearColumns (line 374) | public async clearColumns() {}
    method convertIsFormula (line 375) | public async convertIsFormula() {}
    method sendTableAction (line 376) | public async sendTableAction() {}
    method sendTableActions (line 377) | public async sendTableActions() {}
    method convertToCard (line 378) | public convertToCard() {}
    method getTableModelMaybeWithDiff (line 379) | public getTableModelMaybeWithDiff(tableId: string) {
    method getTableModel (line 383) | public getTableModel(tableId: string) {
    method onSetCursorPos (line 387) | public async onSetCursorPos(rowModel: BaseRowModel | undefined, fieldM...
    method getLinkingRowIds (line 398) | public getLinkingRowIds(sectionId: number): UIRowId[] | undefined {
  type ColDef (line 408) | interface ColDef {

FILE: app/client/components/WidgetFrame.ts
  type WidgetFrameOptions (line 50) | interface WidgetFrameOptions {
  class WidgetFrame (line 100) | class WidgetFrame extends DisposableWithEvents {
    method constructor (line 117) | constructor(private _options: WidgetFrameOptions) {
    method useEvents (line 168) | public useEvents(source: IEventSource, access: AccessChecker) {
    method exposeAPI (line 185) | public exposeAPI(name: string, api: any, access: AccessChecker) {
    method exposeMethod (line 193) | public exposeMethod(name: string, handler: (...args: any[]) => any, ac...
    method editOptions (line 206) | public editOptions() {
    method callRemote (line 213) | public callRemote(name: string, ...args: any[]) {
    method buildDom (line 217) | public buildDom() {
    method _urlWithAccess (line 234) | private _urlWithAccess(url: string | null): string | null {
    method _getEmptyWidgetPage (line 254) | private _getEmptyWidgetPage(): string {
    method _onMessage (line 258) | private _onMessage(event: MessageEvent) {
    method _checkWidgetRepository (line 286) | private async _checkWidgetRepository() {
  function wrapObject (line 305) | function wrapObject<T extends object>(impl: T, accessChecker: AccessChec...
  type AccessChecker (line 327) | interface AccessChecker {
  class MinimumLevel (line 339) | class MinimumLevel implements AccessChecker {
    method constructor (line 340) | constructor(private _minimum: AccessLevel) {}
    method check (line 341) | public check(access: AccessLevel): boolean {
  type MethodMatcher (line 346) | type MethodMatcher<T> = keyof T | "*";
  class MethodAccess (line 368) | class MethodAccess<T> implements AccessChecker {
    method constructor (line 370) | constructor() {}
    method require (line 371) | public require(level: AccessLevel, method: MethodMatcher<T> = "*") {
    method check (line 376) | public check(access: AccessLevel, method?: string): boolean {
  class GristDocAPIImpl (line 408) | class GristDocAPIImpl implements GristDocAPI {
    method constructor (line 413) | constructor(private _doc: GristDoc) {}
    method getDocName (line 415) | public async getDocName() {
    method listTables (line 419) | public async listTables(): Promise<string[]> {
    method fetchTable (line 426) | public async fetchTable(tableId: string) {
    method applyUserActions (line 430) | public async applyUserActions(actions: any[][], options?: any) {
    method getAccessToken (line 443) | public async getAccessToken(options: AccessTokenOptions) {
  class GristViewImpl (line 453) | class GristViewImpl implements GristView {
    method constructor (line 454) | constructor(private _baseView: BaseView, private _access: AccessLevel) {
    method fetchSelectedTable (line 457) | public async fetchSelectedTable(options: FetchSelectedOptions = {}): P...
    method fetchSelectedRecord (line 478) | public async fetchSelectedRecord(rowId: number, options: FetchSelected...
    method allowSelectBy (line 504) | public async allowSelectBy(): Promise<void> {
    method setSelectedRows (line 511) | public async setSelectedRows(rowIds: number[] | null): Promise<void> {
    method setCursorPos (line 515) | public setCursorPos(cursorPos: CursorPos): Promise<void> {
    method _visibleColumns (line 520) | private _visibleColumns(options: FetchSelectedOptions): ColumnRec[] {
  class WidgetAPIImpl (line 554) | class WidgetAPIImpl implements WidgetAPI {
    method constructor (line 555) | constructor(private _section: ViewSectionRec) {}
    method setOptions (line 562) | public async setOptions(options: object): Promise<void> {
    method getOptions (line 569) | public async getOptions(): Promise<Record<string, unknown> | null> {
    method clearOptions (line 573) | public async clearOptions(): Promise<void> {
    method setOption (line 577) | public async setOption(key: string, value: any): Promise<void> {
    method getOption (line 583) | public getOption(key: string): Promise<unknown> {
  constant COMMAND_MINIMUM_ACCESS_LEVELS (line 589) | const COMMAND_MINIMUM_ACCESS_LEVELS = new Map<CommandName, AccessLevel>([
  class CommandAPI (line 595) | class CommandAPI {
    method constructor (line 596) | constructor(private _currentAccess: AccessLevel) {}
    method run (line 598) | public async run(commandName: CommandName): Promise<unknown> {
  type IEventSource (line 624) | interface IEventSource extends DisposableWithEvents {
  class BaseEventSource (line 631) | class BaseEventSource extends DisposableWithEvents implements IEventSour...
    method attach (line 633) | public attach(frame: WidgetFrame): void {
    method _ready (line 637) | protected _ready() {
    method _notify (line 641) | protected _notify(data: any) {
  class RecordNotifier (line 652) | class RecordNotifier extends BaseEventSource {
    method constructor (line 654) | constructor(private _baseView: BaseView) {
    method _update (line 660) | private _update() {
  type ConfigNotifierOptions (line 673) | interface ConfigNotifierOptions {
  class ConfigNotifier (line 680) | class ConfigNotifier extends BaseEventSource {
    method constructor (line 690) | constructor(private _section: ViewSectionRec, private _options: Config...
    method _ready (line 701) | protected _ready() {
    method _update (line 706) | private _update({ fromReady}: { fromReady?: boolean } = {}) {
  class ThemeNotifier (line 722) | class ThemeNotifier extends BaseEventSource {
    method constructor (line 723) | constructor() {
    method _ready (line 734) | protected _ready() {
    method _update (line 738) | private _update({ fromReady}: { fromReady?: boolean } = {}) {
  class TableNotifier (line 753) | class TableNotifier extends BaseEventSource {
    method constructor (line 756) | constructor(private _baseView: BaseView) {
    method _ready (line 770) | protected _ready() {
    method _update (line 775) | private _update() {
  class CustomSectionAPIImpl (line 790) | class CustomSectionAPIImpl extends Disposable implements CustomSectionAPI {
    method constructor (line 791) | constructor(
    method mappings (line 799) | public async mappings(): Promise<WidgetColumnMap | null> {
    method configure (line 807) | public async configure(settings: InteractionOptionsRequest): Promise<v...
  function getReencode (line 825) | function getReencode(cellFormat: CellFormatType | undefined): typeof ree...
  function reencodeAsAny (line 845) | function reencodeAsAny(value: CellValue, typeInfo: GristTypeInfo): CellV...

FILE: app/client/components/buildViewSectionDom.ts
  function buildCollapsedSectionDom (line 22) | function buildCollapsedSectionDom(options: {
  function buildViewSectionDom (line 59) | function buildViewSectionDom(options: {

FILE: app/client/components/commandList.ts
  type CommandName (line 5) | type CommandName =
  type CommandDef (line 134) | interface CommandDef {
  type MenuCommand (line 146) | interface MenuCommand {
  type CommendGroupDef (line 151) | interface CommendGroupDef {

FILE: app/client/components/commands.ts
  type BoolLike (line 21) | type BoolLike = boolean | ko.Observable<boolean> | ko.Computed<boolean> ...
  function subscribe (line 26) | function subscribe(value: Exclude<BoolLike, boolean>, fn: (value: boolea...
  function init (line 61) | function init(optCommandGroups?: CommendGroupDef[]) {
  constant KEY_MAP_MAC (line 95) | const KEY_MAP_MAC = {
  constant KEY_MAP_WIN (line 106) | const KEY_MAP_WIN = {
  function getHumanKey (line 114) | function getHumanKey(key: string, mac: boolean): string {
  type CommandOptions (line 125) | interface CommandOptions {
  class Command (line 142) | class Command implements CommandDef {
    method constructor (line 155) | constructor(name: CommandName, desc: (() => string) | null, keys: stri...
    method getKeysDesc (line 174) | public getKeysDesc() {
    method getDesc (line 183) | public getDesc() {
    method getKeysDom (line 194) | public getKeysDom(separator?: ko.Observable<string>) {
    method addGroup (line 204) | public addGroup(cmdGroup: CommandGroup) {
    method removeGroup (line 212) | public removeGroup(cmdGroup: CommandGroup) {
    method _updateActive (line 220) | private _updateActive() {
    method _run (line 249) | private _run(...args: any[]) {
  function wrapKeyCallback (line 258) | function wrapKeyCallback(callback: Func) {
  type Func (line 266) | type Func = (...args: any[]) => any;
  type CommandMap (line 267) | type CommandMap = { [key in CommandName]?: Func };
  class CommandGroup (line 278) | class CommandGroup extends Disposable {
    method constructor (line 293) | constructor(commands: CommandMap, context: any, activate?: BoolLike) {
    method activate (line 332) | public activate(yesNo: boolean) {
    method _addGroup (line 340) | private _addGroup() {
    method _removeGroup (line 354) | private _removeGroup() {
  type BoundedFunc (line 368) | type BoundedFunc<T> = (this: T, ...args: any[]) => any;
  type BoundedMap (line 369) | type BoundedMap<T> = { [key in CommandName]?: BoundedFunc<T> };
  function createGroup (line 374) | function createGroup<T>(commands: BoundedMap<T> | null, context: T, acti...

FILE: app/client/components/duplicatePage.ts
  function buildDuplicatePageDialog (line 14) | async function buildDuplicatePageDialog(gristDoc: GristDoc, pageId: numb...
  function duplicatePage (line 34) | async function duplicatePage(gristDoc: GristDoc, pageId: number, pageNam...

FILE: app/client/components/duplicateWidget.ts
  type PageSelectOption (line 26) | type PageSelectOption = IOption<number> & { isActivePage: boolean };
  function buildDuplicateWidgetModal (line 28) | async function buildDuplicateWidgetModal(gristDoc: GristDoc, viewSection...
  function duplicateWidgets (line 72) | async function duplicateWidgets(gristDoc: GristDoc, srcViewSectionIds: n...
  function copyFilters (line 151) | async function copyFilters(
  function updateViewSections (line 178) | async function updateViewSections(gristDoc: GristDoc, duplicatedViewSect...
  function copyOriginalViewFields (line 210) | async function copyOriginalViewFields(gristDoc: GristDoc, viewSectionPai...
  function listAllViewFields (line 245) | function listAllViewFields(viewSections: ViewSectionRec[]) {
  function removeViewFields (line 251) | async function removeViewFields(gristDoc: GristDoc, fieldIds: number[]) {
  function createNewViewSections (line 259) | async function createNewViewSections(gristDoc: GristDoc, viewSections: V...
  function newViewSectionAction (line 297) | function newViewSectionAction(widget: IPageWidget, viewId: number) {
  function patchLayoutSpec (line 314) | function patchLayoutSpec(layoutSpec: BoxSpec, mapIds: { [id: number]: nu...
  type DuplicatedViewSection (line 329) | interface DuplicatedViewSection {

FILE: app/client/components/modals.ts
  function buildConfirmDelete (line 26) | function buildConfirmDelete(
  function showDeprecatedWarning (line 70) | function showDeprecatedWarning(
  function reportUndo (line 111) | function reportUndo(
  type ShowTipPopupOptions (line 143) | interface ShowTipPopupOptions {
  function showTipPopup (line 152) | function showTipPopup(
  type ShowNewsPopupOptions (line 210) | interface ShowNewsPopupOptions {
  function showNewsPopup (line 214) | function showNewsPopup(
  function buildArrow (line 268) | function buildArrow() {
  function sideSelectorChunk (line 277) | function sideSelectorChunk(side: "top" | "bottom" | "left" | "right") {
  function fadeInFromSide (line 281) | function fadeInFromSide(side: "top" | "bottom" | "left" | "right") {
  constant HEADER_HEIGHT_PX (line 307) | const HEADER_HEIGHT_PX = 30;

FILE: app/client/components/viewCommon.js
  function makeResizable (line 37) | function makeResizable(widthObservable, options) {

FILE: app/client/declarations.d.ts
  type NewField (line 12) | interface NewField {
  class RecordLayout (line 20) | class RecordLayout extends Disposable {
  type ViewSectionData (line 46) | interface ViewSectionData {
  class ViewConfigTab (line 52) | class ViewConfigTab extends Disposable {
  class BaseRowModel (line 74) | class BaseRowModel extends Disposable {
  type NPartial (line 91) | type NPartial<T> = {
  type Values (line 94) | type Values<T> = T extends keyof SchemaTypes ? NPartial<SchemaTypes[T]> ...
  class MetaRowModel (line 97) | class MetaRowModel<TName extends (keyof SchemaTypes) | undefined = undef...
  type SaveInterface (line 106) | interface SaveInterface<T> {
  type KoSaveableObservable (line 113) | type KoSaveableObservable<T> = ko.Observable<T> & SaveInterface<T>;
  type KoSaveableComputed (line 114) | type KoSaveableComputed<T> = ko.Computed<T> & SaveInterface<T>;
  type CustomComputed (line 116) | interface CustomComputed<T> extends KoSaveableComputed<T> {
  type ObjObservable (line 125) | interface ObjObservable<T extends object> extends ko.Observable<T> {
  type SaveableObjObservable (line 130) | interface SaveableObjObservable<T extends object> extends ko.Observable<...
  class TableModel (line 167) | class TableModel extends RowSource {
  class MetaTableModel (line 194) | class MetaTableModel<RowModel extends MetaRowModel> extends TableModel {
  type LazyArrayModel (line 219) | interface LazyArrayModel<T> extends KoArray<T | null> {
  class DataTableModel (line 228) | class DataTableModel extends TableModel {
  type ComputedWithKoUtils (line 241) | interface ComputedWithKoUtils<T> extends ko.Computed<T> {
  type ObservableWithKoUtils (line 245) | interface ObservableWithKoUtils<T> extends ko.Observable<T> {
  type Location (line 260) | interface Location {
  type JQuery (line 266) | interface JQuery {
  type ResizableOptions (line 272) | interface ResizableOptions {
  type JQueryUI (line 284) | interface JQueryUI {
  type Position (line 294) | interface Position {
  type Size (line 299) | interface Size {

FILE: app/client/lib/ACIndex.ts
  type ACItem (line 17) | interface ACItem {
  function normalizeText (line 25) | function normalizeText(text: string): string {
  type ACIndex (line 38) | interface ACIndex<Item extends ACItem> {
  type HighlightFunc (line 43) | type HighlightFunc = (text: string) => string[];
  type ACResults (line 51) | interface ACResults<Item extends ACItem> {
  type Word (line 65) | interface Word {
  type ACIndexOptions (line 71) | interface ACIndexOptions {
  class ACIndexImpl (line 88) | class ACIndexImpl<Item extends ACItem> implements ACIndex<Item> {
    method totalItems (line 98) | public get totalItems() {
    method constructor (line 103) | constructor(items: Item[], private _options: ACIndexOptions = {}) {
    method search (line 122) | public search(searchText: string): ACResults<Item> {
    method _findOverlaps (line 195) | private _findOverlaps(searchWord: string, searchWordPos: number): Map<...
  type BuildHighlightFunc (line 241) | type BuildHighlightFunc = (match: string) => DomContents;
  function buildHighlightedDom (line 246) | function buildHighlightedDom(
  function highlightMatches (line 261) | function highlightMatches(searchWords: string[], text: string): string[] {
  function findLongestPrefixLen (line 283) | function findLongestPrefixLen(text: string, choices: string[]): number {
  function findCommonPrefixLength (line 287) | function findCommonPrefixLength(text1: string, text2: string): number {
  function startsWithText (line 297) | function startsWithText(item: ACItem, text: string, searchWords: string[...

FILE: app/client/lib/ACSelect.ts
  type ACSelectItem (line 9) | interface ACSelectItem extends ACItem {
  function buildACSelect (line 19) | function buildACSelect(

FILE: app/client/lib/ACUserManager.ts
  type ACUserItem (line 26) | interface ACUserItem extends ACItem {
  function buildACMemberEmail (line 36) | function buildACMemberEmail(

FILE: app/client/lib/BoxSpec.ts
  type BoxSpec (line 6) | interface BoxSpec {
  function purgeBoxSpec (line 13) | function purgeBoxSpec(options: {
  function addToSpec (line 50) | function addToSpec(tmpLayout: Layout, leafId: number) {

FILE: app/client/lib/CellDiffTool.ts
  class CellDiffTool (line 7) | class CellDiffTool {
    method prepareCellDiff (line 27) | public prepareCellDiff(value: CellValue, formatter: BaseFormatter): Di...
    method _prepareTextDiff (line 57) | private _prepareTextDiff(txt1: string, txt2: string): Diff[] {
    method _notDiffWorthy (line 74) | private _notDiffWorthy(txt: string, parts: number) {
    method _isMostlyNumeric (line 79) | private _isMostlyNumeric(txt: string) {
  constant DIFF_LOCAL (line 86) | const DIFF_LOCAL = 2;

FILE: app/client/lib/CustomSectionElement.ts
  type PluginCustomSection (line 9) | interface PluginCustomSection {
  class CustomSectionElement (line 14) | class CustomSectionElement {
    method getSections (line 18) | public static getSections(plugins: PluginInstance[]): PluginCustomSect...
    method find (line 36) | public static find(plugin: PluginInstance, sectionName: string): ViewP...

FILE: app/client/lib/Delay.ts
  class Delay (line 8) | class Delay extends Disposable {
    method wrapWithDelay (line 16) | public static wrapWithDelay<T>(ms: number, cb: (this: T, ...args: any[...
    method untilAnimationFrame (line 30) | public static untilAnimationFrame<T>(cb: (this: T, ...args: any[]) => ...
    method create (line 51) | public create() {
    method cancel (line 58) | public cancel() {
    method isPending (line 68) | public isPending() {
    method schedule (line 78) | public schedule<T>(ms: number, cb: (this: T, ...args: any[]) => any, o...
  type DisposableCB (line 87) | interface DisposableCB {

FILE: app/client/lib/DocPluginManager.ts
  class DocPluginManager (line 12) | class DocPluginManager {
    method constructor (line 20) | constructor(private _options: {
    method receiveAction (line 56) | public receiveAction(action: any[]) {
    method makeAnonForwarder (line 68) | public makeAnonForwarder() {

FILE: app/client/lib/DocSchemaImport.ts
  function getExistingDocSchema (line 5) | async function getExistingDocSchema(docApi: DocAPI): Promise<ExistingDoc...

FILE: app/client/lib/FocusLayer.ts
  type FocusLayerOptions (line 21) | interface FocusLayerOptions {
  class FocusLayerManager (line 52) | class FocusLayerManager extends Disposable {
    method constructor (line 56) | constructor() {
    method addLayer (line 81) | public addLayer(layer: FocusLayer) {
    method removeLayer (line 89) | public removeLayer(layer: FocusLayer) {
    method getCurrentLayer (line 95) | public getCurrentLayer(): FocusLayer | undefined {
    method grabFocus (line 102) | public grabFocus() {
    method _doGrabFocus (line 108) | private _doGrabFocus() {
  class FocusLayer (line 134) | class FocusLayer extends Disposable implements FocusLayerOptions {
    method grabFocus (line 136) | public static grabFocus() {
    method attach (line 144) | public static attach(options: Partial<FocusLayerOptions>): DomMethod<H...
    method constructor (line 157) | constructor(options: FocusLayerOptions) {
    method onDefaultFocus (line 181) | public onDefaultFocus() {
    method onDefaultBlur (line 188) | public onDefaultBlur() {
  function watchElementForBlur (line 201) | function watchElementForBlur(elem: Element, callback: () => void) {

FILE: app/client/lib/GristWindow.ts
  type Window (line 18) | interface Window {

FILE: app/client/lib/HomePluginManager.ts
  class HomePluginManager (line 12) | class HomePluginManager {
    method constructor (line 15) | constructor(options: {
  class NotAvailableForwarder (line 58) | class NotAvailableForwarder {
    method forwardPluginRpc (line 59) | public async forwardPluginRpc(pluginId: string, msg: any) {

FILE: app/client/lib/ImportSourceElement.ts
  class ImportSourceElement (line 11) | class ImportSourceElement {
    method fromArray (line 15) | public static fromArray(pluginInstances: PluginInstance[]): ImportSour...
    method constructor (line 30) | private constructor(public plugin: PluginInstance, public importSource...

FILE: app/client/lib/MultiUserManager.ts
  function parseEmailList (line 13) | function parseEmailList(emailListRaw: string): string[] {
  function validateEmail (line 20) | function validateEmail(email: string): boolean {
  function buildMultiUserManagerModal (line 25) | function buildMultiUserManagerModal(
  function buildRolesSelect (line 82) | function buildRolesSelect(
  function buildEmailsTextarea (line 106) | function buildEmailsTextarea(

FILE: app/client/lib/ObservableMap.js
  function ObservableMap (line 37) | function ObservableMap(mapFunc) {

FILE: app/client/lib/ObservableSet.js
  function ObservableSet (line 10) | function ObservableSet() {

FILE: app/client/lib/ReferenceUtils.ts
  class ReferenceUtils (line 20) | class ReferenceUtils extends Disposable {
    method constructor (line 33) | constructor(public readonly field: ViewFieldRec, private readonly _gri...
    method idToText (line 57) | public idToText(value: unknown) {
    method autocompleteSearch (line 72) | public autocompleteSearch(text: string, rowId: number): ACResults<ICel...
    method buildNoItemsMessage (line 90) | public buildNoItemsMessage() {
    method _getDropdownConditionACIndex (line 114) | private _getDropdownConditionACIndex(rowId: number) {
    method _buildDropdownConditionACFilter (line 125) | private _buildDropdownConditionACFilter(rowId: number) {
  function nocaseEqual (line 147) | function nocaseEqual(a: string, b: string) {

FILE: app/client/lib/SafeBrowser.ts
  class SafeBrowser (line 59) | class SafeBrowser extends BaseComponent {
    method createWorker (line 63) | public static createWorker(safeBrowser: SafeBrowser, rpc: Rpc, src: st...
    method createView (line 70) | public static createView(safeBrowser: SafeBrowser, rpc: Rpc, src: stri...
    method constructor (line 91) | constructor(private _options: {
    method createViewProcess (line 113) | public createViewProcess(path: string): ViewProcess {
    method receiveAction (line 120) | public receiveAction(action: any[]) {
    method renderImpl (line 130) | public async renderImpl(path: string, target: RenderTarget, options: R...
    method disposeImpl (line 156) | public async disposeImpl(procId: number): Promise<void> {
    method doForwardCall (line 164) | protected doForwardCall(c: IMsgRpcCall): Promise<any> {
    method doForwardMessage (line 172) | protected doForwardMessage(c: IMsgCustom): Promise<any> {
    method activateImplementation (line 180) | protected async activateImplementation(): Promise<void> {
    method deactivateImplementation (line 191) | protected async deactivateImplementation(): Promise<void> {
    method _createViewProcess (line 202) | private _createViewProcess(path: string): [ViewProcess, number] {
    method _createRpc (line 224) | private _createRpc(path: string): Rpc {
  class ClientProcess (line 240) | class ClientProcess extends Disposable {
    method create (line 248) | public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {
    method receiveAction (line 265) | public receiveAction(action: any[]) {
  class WorkerProcess (line 274) | class WorkerProcess extends ClientProcess  {
    method create (line 275) | public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {
  class ViewProcess (line 285) | class ViewProcess extends ClientProcess {
  class IframeProcess (line 295) | class IframeProcess extends ViewProcess {
    method create (line 296) | public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {
    method _sendTheme (line 331) | private async _sendTheme({ theme, fromReady = false}: { theme: Theme, ...
  class WebviewProcess (line 339) | class WebviewProcess extends ViewProcess {
    method create (line 340) | public create(safeBrowser: SafeBrowser, rpc: Rpc, src: string) {

FILE: app/client/lib/Signal.ts
  class Signal (line 37) | class Signal<T = any> implements IDisposable, IDisposableOwner {
    method create (line 42) | public static create<T>(owner: IDisposableOwner | null, value: T) {
    method fromEvents (line 49) | public static fromEvents<T = any>(
    method compute (line 65) | public static compute<T>(owner: Disposable | null, compute: ComputeFun...
    method constructor (line 95) | constructor(owner: IDisposableOwner | null, initialValue: T) {
    method dispose (line 100) | public dispose() {
    method autoDispose (line 104) | public autoDispose(disposable: IDisposable) {
    method pipe (line 111) | public pipe(signal: Signal<T>) {
    method map (line 119) | public map<Z>(selector: (value: T) => Z): Signal<Z> {
    method filter (line 131) | public filter(selector: (value: T) => boolean): Signal<T> {
    method distinct (line 144) | public distinct(): Signal<T> {
    method flag (line 160) | public flag() {
    method listen (line 167) | public listen(handler: (value: T) => any) {
    method emit (line 177) | public emit(value: T) {
    method before (line 189) | public before(handler: CustomEmitter<T>) {
  type ComputeFunction (line 194) | type ComputeFunction<T> = (on: <TS>(s: Signal<TS>) => TS) => T;
  type CustomEmitter (line 195) | type CustomEmitter<T> = (value: T, emit: (value: T) => void) => any;

FILE: app/client/lib/Suggestions.ts
  type ISuggestionWithSubAttrs (line 8) | interface ISuggestionWithSubAttrs {
  function expandAndFilterSuggestions (line 21) | function expandAndFilterSuggestions(
  function findMatchingSuggestion (line 55) | function findMatchingSuggestion(text: string, suggestions: ISuggestionWi...
  function splitAttr (line 66) | function splitAttr(text: string): [string, string] {

FILE: app/client/lib/TokenField.ts
  type IToken (line 28) | interface IToken {
  type ITokenFieldOptions (line 32) | interface ITokenFieldOptions<Token extends IToken> {
  type ITokenFieldKeyBindings (line 58) | interface ITokenFieldKeyBindings {
  type ITokenFieldVariant (line 63) | type ITokenFieldVariant = "horizontal" | "vertical";
  class TokenWrap (line 71) | class TokenWrap<Token extends IToken> {
    method constructor (line 72) | constructor(public token: Token) {}
  class UndoItem (line 75) | class UndoItem {
    method constructor (line 76) | constructor(public redo: () => void, public undo: () => void) {}
  class TokenField (line 79) | class TokenField<Token extends IToken = IToken> extends Disposable {
    method ctor (line 80) | public static ctor<T extends IToken>(): IDisposableCtor<TokenField<T>,...
    method constructor (line 107) | constructor(private _options: ITokenFieldOptions<Token>) {
    method attach (line 202) | public attach(elem: HTMLElement): void {
    method getRootElem (line 207) | public getRootElem(): HTMLElement {
    method getTextInput (line 212) | public getTextInput(): HTMLInputElement {
    method getTextInputValue (line 219) | public getTextInputValue(): string {
    method getHiddenInput (line 224) | public getHiddenInput(): HTMLInputElement {
    method getAutocomplete (line 231) | public getAutocomplete(): Autocomplete<Token & ACItem> | null {
    method setTokens (line 243) | public setTokens(tokens: Token[]): void {
    method replaceToken (line 249) | public replaceToken(label: string, newToken: Token): void {
    method _openAutocomplete (line 256) | private _openAutocomplete() {
    method _addSelectedItem (line 266) | private _addSelectedItem(): boolean {
    method _onInputFocus (line 282) | private _onInputFocus() {
    method _onTokenClick (line 292) | private _onTokenClick(ev: MouseEvent, t: TokenWrap<Token>) {
    method _maybeSelectAllTokens (line 317) | private _maybeSelectAllTokens(ev: KeyboardEvent) {
    method _setFocus (line 329) | private _setFocus() {
    method _maybeBackspace (line 337) | private _maybeBackspace(ev: KeyboardEvent) {
    method _maybeDelete (line 350) | private _maybeDelete(ev: KeyboardEvent) {
    method _maybeAdvance (line 359) | private _maybeAdvance(ev: KeyboardEvent, advance: 1 | -1): void {
    method _toggleTokenSelection (line 399) | private _toggleTokenSelection(token: TokenWrap<Token>) {
    method _resetTokenSelection (line 410) | private _resetTokenSelection(token: TokenWrap<Token> | null) {
    method _deleteTokens (line 416) | private _deleteTokens(toDelete: Set<TokenWrap<Token>>, advance: 1 | -1...
    method _getNextToken (line 424) | private _getNextToken(selection: Set<TokenWrap<Token>>, advance: 1 | -...
    method _getSelectedIndexRange (line 430) | private _getSelectedIndexRange(selection: Set<TokenWrap<Token>>): [num...
    method _onCopyEvent (line 442) | private _onCopyEvent(ev: ClipboardEvent): boolean {
    method _onCutEvent (line 457) | private _onCutEvent(ev: ClipboardEvent) {
    method _onPasteEvent (line 463) | private _onPasteEvent(ev: ClipboardEvent) {
    method _onMouseDown (line 495) | private _onMouseDown(startEvent: MouseEvent, t: TokenWrap<Token>) {
    method _recordUndo (line 579) | private _recordUndo(val: TokenWrap<Token>[], prev: TokenWrap<Token>[],...
    method _combineUndo (line 589) | private _combineUndo(callback: () => void) {
    method _undo (line 605) | private _undo(ev: KeyboardEvent): void {
    method _redo (line 619) | private _redo(ev: KeyboardEvent): void {
    method _maybeTrimTokens (line 636) | private _maybeTrimTokens(tokens: Token[]): Token[] {
    method _getNonEmptyTokens (line 644) | private _getNonEmptyTokens(tokens: Token[]): Token[] {
  type ITokenFieldStyles (line 795) | type ITokenFieldStyles = Partial<typeof tokenFieldStyles>;

FILE: app/client/lib/UrlState.ts
  type UrlStateSpec (line 23) | interface UrlStateSpec<IUrlState> {
  type UpdateFunc (line 37) | type UpdateFunc<IUrlState> = (prevState: IUrlState) => IUrlState;
  class UrlState (line 42) | class UrlState<IUrlState extends object> extends Disposable {
    method constructor (line 46) | constructor(private _window: HistWindow, private _stateImpl: UrlStateS...
    method pushUrl (line 62) | public async pushUrl(urlState: IUrlState | UpdateFunc<IUrlState>,
    method makeUrl (line 111) | public makeUrl(urlState: IUrlState | UpdateFunc<IUrlState>, use: UseCB...
    method setHref (line 121) | public setHref(urlState: IUrlState | UpdateFunc<IUrlState>): DomElemen...
    method setLinkUrl (line 134) | public setLinkUrl(
    method loadState (line 158) | public loadState() {
    method _getState (line 163) | private _getState(): IUrlState {
    method _mergeState (line 167) | private _mergeState(prevState: IUrlState, newState: IUrlState | Update...
  type HistWindow (line 175) | interface HistWindow extends EventTarget {
  type UseCB (line 185) | type UseCB = <T>(obs: BaseObservable<T>) => T;

FILE: app/client/lib/Validator.ts
  type ValidationFunction (line 29) | type ValidationFunction = () => (boolean | Promise<boolean> | void | Pro...
  class ValidationGroup (line 35) | class ValidationGroup {
    method validate (line 41) | public async validate() {
    method add (line 66) | public add(validator: Validator) {
    method inputReset (line 73) | public inputReset() {
    method reset (line 80) | public reset() {
  class Validator (line 88) | class Validator extends Disposable {
    method constructor (line 91) | constructor(public group: ValidationGroup, message: string, public che...
    method inputReset (line 100) | public inputReset() {
    method set (line 108) | public set(isValid: boolean | string) {
    method buildDom (line 118) | public buildDom() {

FILE: app/client/lib/airtable/AirtableImportUI.ts
  type AirtableBase (line 50) | interface AirtableBase {
  type AirtableToGristMapping (line 56) | interface AirtableToGristMapping {
  type NewTable (line 62) | interface NewTable {
  type ExistingTable (line 67) | interface ExistingTable {
  type TokenPayload (line 72) | interface TokenPayload {
  type AirtableImportStep (line 78) | type AirtableImportStep = "auth" | "select-base" | "select-tables";
  type Destination (line 80) | type Destination = ExistingDoc & { docSchema?: Computed<ExistingDocSchem...
  type AirtableImportOptions (line 82) | interface AirtableImportOptions {
  class AirtableImport (line 90) | class AirtableImport extends Disposable {
    method constructor (line 197) | constructor(private _options: AirtableImportOptions) {
    method buildDom (line 202) | public buildDom() {
    method _buildAuthDialog (line 219) | private _buildAuthDialog() {
    method _buildSelectAuthMethodScreen (line 235) | private _buildSelectAuthMethodScreen() {
    method _buildPersonalAccessTokenScreen (line 273) | private _buildPersonalAccessTokenScreen() {
    method _connectionMenu (line 304) | private _connectionMenu() {
    method _basesList (line 320) | private _basesList(owner: IDisposableOwner) {
    method _buildBaseTables (line 365) | private _buildBaseTables() {
    method _tableMappingsList (line 422) | private _tableMappingsList(mappings: AirtableToGristMapping[]) {
    method _tableMapping (line 431) | private _tableMapping(mapping: AirtableToGristMapping) {
    method _destinationMenu (line 447) | private _destinationMenu(mapping: AirtableToGristMapping) {
    method _destinationMenuLabel (line 459) | private _destinationMenuLabel(mapping: AirtableToGristMapping) {
    method _destinationMenuOptions (line 489) | private _destinationMenuOptions(mapping: AirtableToGristMapping) {
    method _tableWarnings (line 533) | private _tableWarnings({ tableId }: AirtableToGristMapping) {
    method _checkForToken (line 551) | private _checkForToken() {
    method _handleOAuthLogin (line 576) | private _handleOAuthLogin() {
    method _handleTokenPayload (line 602) | private _handleTokenPayload(payload: TokenPayload) {
    method _handlePersonalAccessTokenLogin (line 614) | private async _handlePersonalAccessTokenLogin() {
    method _handleSelectBase (line 626) | private async _handleSelectBase(baseId: string) {
    method _destination (line 633) | private _destination(): AirtableImportDestination {
    method _handleImport (line 644) | private _handleImport() {
    method _doAsyncWork (line 683) | private async _doAsyncWork(
    method _fetchBases (line 703) | private _fetchBases(token: string) {
    method _fetchBaseSchema (line 715) | private _fetchBaseSchema() {
    method _handleLogout (line 739) | private _handleLogout() {
    method _handleRefresh (line 750) | private _handleRefresh() {
  class OAuth2ClientsAPI (line 760) | class OAuth2ClientsAPI extends BaseAPI {
    method constructor (line 762) | constructor(homeUrl: string = getHomeUrl()) {
    method fetchToken (line 767) | public fetchToken(): Promise<TokenPayload> { return this.requestJson(`...
    method deleteToken (line 768) | public deleteToken() { return this.requestJson(`${this._homeUrl}/oauth...
  function cssWarning (line 771) | function cssWarning(...args: DomElementArg[]) {
  function cssError (line 776) | function cssError(...args: DomElementArg[]) {

FILE: app/client/lib/airtable/AirtableImporter.ts
  type ExistingDoc (line 20) | interface ExistingDoc {
  type NewDoc (line 25) | interface NewDoc {
  type AirtableImportDestination (line 31) | type AirtableImportDestination = NewDoc | ExistingDoc;
  type AirtableImportOptions (line 33) | interface AirtableImportOptions {
  type AirtableImportResult (line 40) | interface AirtableImportResult {
  function applyAirtableImportSchemaAndImportData (line 48) | async function applyAirtableImportSchemaAndImportData(params: {
  function validateAirtableSchemaImport (line 146) | function validateAirtableSchemaImport(

FILE: app/client/lib/airtable/startDocAirtableImport.ts
  function startDocAirtableImport (line 11) | async function startDocAirtableImport(gristDoc: GristDoc) {

FILE: app/client/lib/airtable/startHomeAirtableImport.ts
  function startHomeAirtableImport (line 13) | async function startHomeAirtableImport(home: HomeModel) {

FILE: app/client/lib/autocomplete.ts
  type IAutocompleteOptions (line 17) | interface IAutocompleteOptions<Item extends ACItem> {
  class Autocomplete (line 54) | class Autocomplete<Item extends ACItem> extends Disposable {
    method constructor (line 72) | constructor(
    method getSelectedItem (line 122) | public getSelectedItem(): Item | undefined {
    method search (line 126) | public search(findMatch?: (items: Item[]) => number) {
    method _value (line 130) | private get _value() {
    method _value (line 139) | private set _value(value: string) {
    method _setSelected (line 151) | private _setSelected(index: number, updateValue: boolean) {
    method _findTargetItem (line 175) | private _findTargetItem(target: EventTarget | null): number {
    method _getNext (line 181) | private _getNext(step: 1 | -1): number {
    method _updateChoices (line 188) | private async _updateChoices(inputVal: string, findMatch?: (items: Ite...
    method _allItems (line 212) | private get _allItems() {
    method _maybeShowNoItemsMessage (line 216) | private _maybeShowNoItemsMessage() {
  method fn (line 236) | fn({ state }: any) {
  function getContainer (line 258) | function getContainer(elem: Element, attachElem: Element | string | null...

FILE: app/client/lib/browserGlobals.ts
  type OrigGlobals (line 8) | type OrigGlobals = typeof globalThis;
  type Globals (line 10) | interface Globals extends Partial<OrigGlobals> {
  type PossibleNames (line 15) | type PossibleNames = keyof Globals;
  type RequestedGlobals (line 17) | interface RequestedGlobals {
  function get (line 37) | function get<Names extends PossibleNames[]>(...neededNames: Names): Requ...
  function updateGlobals (line 52) | function updateGlobals(obj: RequestedGlobals) {
  function setGlobals (line 62) | function setGlobals(globals: Globals) {

FILE: app/client/lib/browserInfo.ts
  function getParser (line 5) | function getParser() {
  function isDesktop (line 10) | function isDesktop() {
  function isIOS (line 18) | function isIOS() {
  function modKeyProp (line 26) | function modKeyProp(): "metaKey" | "ctrlKey" {
  function isWin (line 30) | function isWin() {

FILE: app/client/lib/chartUtil.ts
  function sortByXValues (line 12) | function sortByXValues(series: { values: Datum[] }[]): void {
  function uniqXValues (line 28) | function uniqXValues<T extends { values: Datum[] }>(series: T[]) {
  function splitValues (line 40) | function splitValues<T extends { values: Datum[] }>(series: T[]): T[] {
  function splitValuesByIndex (line 45) | function splitValuesByIndex<T extends { values: Datum[] }>(series: T[], ...
  function consolidateValues (line 72) | function consolidateValues(series: { values: Datum[] }[], xvalues: Datum...
  function formatPercent (line 89) | function formatPercent(val: number) {

FILE: app/client/lib/clipboardUtils.ts
  function copyToClipboard (line 8) | async function copyToClipboard(data: string | ClipboardItem) {
  function copyTextToClipboard (line 19) | async function copyTextToClipboard(txt: string) {
  function copyDataToClipboard (line 54) | async function copyDataToClipboard(data: ClipboardItem) {
  function readTextFromClipboard (line 65) | function readTextFromClipboard(): Promise<string> {
  function readDataFromClipboard (line 76) | function readDataFromClipboard(): Promise<ClipboardItem[]> {

FILE: app/client/lib/dblclick.ts
  constant DOUBLE_TAP_INTERVAL_MS (line 3) | const DOUBLE_TAP_INTERVAL_MS = 500;
  function onDblClickMatchElem (line 22) | function onDblClickMatchElem(elem: EventTarget, selector: string, callba...

FILE: app/client/lib/dispose.js
  class Disposable (line 68) | class Disposable {
    method constructor (line 72) | constructor(...args) {
    method create (line 80) | static create(...args) {
  function makeDisposable (line 211) | function makeDisposable(Constructor) {
  function safeCreate (line 225) | function safeCreate(Constructor, varArgs) {
  function wipeOutObject (line 279) | function wipeOutObject(obj) {
  function removeObjectToDispose (line 291) | function removeObjectToDispose(disposalList, obj) {
  function methodDisposer (line 316) | function methodDisposer(methodName) {
  function disposeHelper (line 327) | function disposeHelper(owner, disposer, obj) {
  function defaultDisposer (line 339) | function defaultDisposer(obj) {
  function emptyNode (line 354) | function emptyNode(node) {

FILE: app/client/lib/dom.js
  function dom (line 39) | function dom(firstArg, ...args) {
  function createElemFromString (line 64) | function createElemFromString(tagString, elemCreator) {
  function createDOMElement (line 91) | function createDOMElement(tagName) {
  function createSVGElement (line 95) | function createSVGElement(tagName) {
  function handleChildren (line 100) | function handleChildren(elem, children, index) {
  function wrapInlinable (line 185) | function wrapInlinable(func, context, args) {
  function makeFilterFunc (line 316) | function makeFilterFunc(selectorOrFunc) {

FILE: app/client/lib/domAsync.ts
  function domAsync (line 12) | function domAsync(promiseForDomContents: Promise<DomContents>, onError =...

FILE: app/client/lib/domUtils.ts
  function makeTestId (line 8) | function makeTestId(prefix: string) {
  function autoFocus (line 19) | function autoFocus() {
  function autoSelect (line 23) | function autoSelect() {
  method create (line 31) | create<T>(owner: IDisposableOwner, cb: (use: UseCB) => Promise<T>): Asyn...
  type AsyncComputed (line 53) | interface AsyncComputed<T> extends Observable<T | undefined> {
  function stopEvent (line 63) | function stopEvent(ev: Event) {
  function domOnCustom (line 72) | function domOnCustom(name: string, handler: (args: any, event: Event, el...
  function domDispatch (line 84) | function domDispatch(element: Element, name: string, args?: any) {
  function onClickOutside (line 99) | function onClickOutside(click: () => void) {
  function onClickOutsideElem (line 113) | function onClickOutsideElem(elem: Node, click: () => void) {
  function findAncestorChild (line 131) | function findAncestorChild(ancestor: Element, elem: Element | null): Ele...
  function attachMouseOverOnMove (line 144) | function attachMouseOverOnMove<T extends EventTarget>(elem: T, callback:...

FILE: app/client/lib/download.js
  function download (line 19) | function download(href) {

FILE: app/client/lib/formUtils.ts
  type SubmitOptions (line 9) | interface SubmitOptions<T> {
  function handleSubmit (line 29) | function handleSubmit<T>(
  function formDataToObj (line 62) | function formDataToObj(formElem: HTMLFormElement): { [key: string]: stri...
  function submitForm (line 78) | async function submitForm(fields: { [key: string]: string }, form: HTMLF...
  function handleFormError (line 86) | function handleFormError(err: unknown, errObs: Observable<string | null>) {
  class TypedFormData (line 101) | class TypedFormData {
    method constructor (line 104) | constructor(private _formElement: HTMLFormElement) {
    method keys (line 108) | public keys() {
    method type (line 122) | public type(key: string) {
    method get (line 126) | public get(key: string) {
    method set (line 134) | public set(key: string, value: any) {
    method getAll (line 138) | public getAll(key: string) {
  function typedFormDataToJson (line 151) | function typedFormDataToJson(formData: TypedFormData) {

FILE: app/client/lib/formatUtils.ts
  function dateFmt (line 6) | function dateFmt(timestamp: number | null | string | Date): string {
  function dateFmtFull (line 20) | function dateFmtFull(timestamp: number | null | string | Date): string {
  function timeFmt (line 28) | function timeFmt(timestampMs: number): string {

FILE: app/client/lib/fromKoSave.ts
  function fromKoSave (line 16) | function fromKoSave<T>(koObs: IKnockoutObservable<T>): Observable<T> {
  class KoSaveWrapObs (line 20) | class KoSaveWrapObs<T> extends KoWrapObs<T> {
    method constructor (line 21) | constructor(_koObs: IKnockoutObservable<T>) {
    method set (line 28) | public set(value: T): void {

FILE: app/client/lib/getOrCreateStyleElement.ts
  function getOrCreateStyleElement (line 11) | function getOrCreateStyleElement(id: string, insertOptions: {

FILE: app/client/lib/guessTimezone.ts
  function guessTimezone (line 7) | async function guessTimezone() {

FILE: app/client/lib/hashUtils.ts
  function hashCode (line 4) | function hashCode(str: string): number {

FILE: app/client/lib/helpScout.ts
  type BeaconCmd (line 28) | type BeaconCmd = "init" | "destroy" | "open" | "close" | "toggle" | "sea...
  type BeaconRoute (line 32) | type BeaconRoute = "/ask/message/" | "/answers/";
  type IUserObj (line 34) | interface IUserObj {
  type IFormObj (line 44) | interface IFormObj {
  type ISessionData (line 52) | interface ISessionData {
  type ICallbackAttributes (line 56) | interface ICallbackAttributes {
  function Beacon (line 77) | function Beacon(method: BeaconCmd, options?: unknown, data?: unknown) {
  function _beacon (line 84) | function _beacon(method: BeaconCmd, options?: unknown, data?: unknown) {
  function initBeacon (line 89) | function initBeacon(): void {
  function _beaconOpen (line 126) | function _beaconOpen(userObj: IUserObj | null, options: IBeaconOpenOptio...
  function fixBeaconBaseHref (line 206) | function fixBeaconBaseHref() {
  type IBeaconOpenOptions (line 220) | interface IBeaconOpenOptions {
  function beaconOpenMessage (line 236) | function beaconOpenMessage(options: IBeaconOpenOptions) {
  function getBeaconUserObj (line 245) | function getBeaconUserObj(appModel: AppModel | null): IUserObj | null {

FILE: app/client/lib/imports.d.ts
  type Ace (line 17) | type Ace = typeof ace;
  type MomentTimezone (line 18) | type MomentTimezone = typeof momentTimezone;
  type PlotlyType (line 19) | type PlotlyType = typeof plotly;

FILE: app/client/lib/koArray.d.ts
  class KoArray (line 3) | class KoArray<T> {

FILE: app/client/lib/koArray.js
  function koArray (line 31) | function koArray(optInitialValues) {
  function SyncedState (line 75) | function SyncedState(constructFunc, key) {
  function KoArray (line 112) | function KoArray(initialValues) {
  function noop (line 324) | function noop() {}
  function identity (line 325) | function identity(x) { return x; }

FILE: app/client/lib/koArrayWrap.ts
  function createObsArray (line 12) | function createObsArray<T>(
  class KoWrapObsArray (line 26) | class KoWrapObsArray<T> extends MutableObsArray<T> {
    method constructor (line 29) | constructor(_koArray: KoArray<T>) {
    method dispose (line 38) | public dispose(): void {

FILE: app/client/lib/koDom.js
  function setBinding (line 37) | function setBinding(elem, valueOrFunc, updaterFunc) {
  function makeBinding (line 70) | function makeBinding(valueOrFunc, updaterFunc) {
  function text (line 82) | function text(valueOrFunc) {
  function bootstrapToken (line 107) | function bootstrapToken(templateToken, valueOrFunc) {
  function attr (line 122) | function attr(attrName, valueOrFunc) {
  function boolAttr (line 139) | function boolAttr(attrName, valueOrFunc) {
  function style (line 156) | function style(property, valueOrFunc) {
  function show (line 177) | function show(boolValueOrFunc) {
  function hide (line 189) | function hide(boolValueOrFunc) {
  function domData (line 199) | function domData(key, valueOrFunc) {
  function value (line 210) | function value(valueOrFunc) {
  function toggleClass (line 226) | function toggleClass(className, boolValueOrFunc) {
  function toggleDisabled (line 240) | function toggleDisabled(boolValueOrFunc) {
  function cssClass (line 257) | function cssClass(valueOrFunc) {
  function scrollChildIntoView (line 282) | function scrollChildIntoView(valueOrFunc) {
  function doScrollChildIntoView (line 287) | function doScrollChildIntoView(elem, index, sync) {
  function scope (line 363) | function scope(valueOrFunc, contentFunc) {
  function maybe (line 407) | function maybe(boolValueOrFunc, contentFunc) {
  function foreach (line 431) | function foreach(data, itemCreateFunc) {

FILE: app/client/lib/koDomScrolly.js
  function ScrollyPane (line 30) | function ScrollyPane(scrolly, paneIndex, container, options, itemCreateF...
  function Scrolly (line 154) | function Scrolly(dataModel) {
  function getInstance (line 255) | function getInstance(dataModel) {
  function scrolly (line 647) | function scrolly(data, options, itemCreateFunc) {

FILE: app/client/lib/koForm.js
  function genSpinner (line 245) | function genSpinner(valueObservable, getNewValue, shouldDisable) {
  function getNewValue (line 297) | function getNewValue(value, direction) {
  function shouldDisable (line 307) | function shouldDisable(value, direction) {
  function getNewValue (line 318) | function getNewValue(value, direction) {
  function shouldDisable (line 324) | function shouldDisable(value, direction) {
  function handleReorderStop (line 513) | function handleReorderStop(container, e, ui) {
  function handleConnectedStop (line 528) | function handleConnectedStop(e, ui) {
  function revertRemovedItem (line 554) | function revertRemovedItem(ui, parent, item, err) {
  function getDraggableItemModel (line 571) | function getDraggableItemModel(elem) {
  function getNextDraggableItemModel (line 578) | function getNextDraggableItemModel(elem) {
  function resetDraggedItem (line 582) | function resetDraggedItem(elem) {
  function enableDraggableConnection (line 588) | function enableDraggableConnection(draggable) {
  function connectDraggableToClass (line 602) | function connectDraggableToClass(draggable, className) {
  function textInput (line 719) | function textInput(valueObservable, options, moreArgs) {
  function _initTabs (line 1041) | function _initTabs(optObservable, labelSelector, elem) {
  function _addTab (line 1057) | function _addTab(tabsSelector, labelElem, contentElem) {

FILE: app/client/lib/koUtil.js
  function withKoUtils (line 8) | function withKoUtils(obj) {
  function _wrapComputedRead (line 83) | function _wrapComputedRead(readFunc) {
  function setComputedErrorHandler (line 107) | function setComputedErrorHandler(handlerFunc) {
  function observableWithDefault (line 129) | function observableWithDefault(obs, defaultOrFunc, optContext) {
  function observableNumber (line 152) | function observableNumber(obs) {
  function computedAutoDispose (line 168) | function computedAutoDispose(optionsOrReadFunc, target, options) {
  function computedBuilder (line 225) | function computedBuilder(callback, optContext) {

FILE: app/client/lib/loadScript.ts
  function loadScript (line 7) | function loadScript(url: string) {
  function loadCssFile (line 18) | function loadCssFile(url: string): Promise<void> {

FILE: app/client/lib/localStorageObs.ts
  function getStorageBoolObs (line 6) | function getStorageBoolObs(store: Storage, key: string, defValue: boolea...
  function localStorageBoolObs (line 19) | function localStorageBoolObs(key: string, defValue = false): Observable<...
  function sessionStorageBoolObs (line 26) | function sessionStorageBoolObs(key: string, defValue = false): Observabl...
  function getStorageObs (line 30) | function getStorageObs(store: Storage, key: string, defaultValue?: strin...
  function localStorageObs (line 39) | function localStorageObs(key: string, defaultValue?: string): Observable...
  function sessionStorageObs (line 46) | function sessionStorageObs(key: string, defaultValue?: string): Observab...
  function getStorageJsonObs (line 50) | function getStorageJsonObs<T>(store: Storage, key: string, defaultValue:...
  function localStorageJsonObs (line 60) | function localStorageJsonObs<T>(key: string, defaultValue: T): Observabl...
  function sessionStorageJsonObs (line 67) | function sessionStorageJsonObs<T>(key: string, defaultValue: T): Observa...

FILE: app/client/lib/localization.ts
  function setupLocale (line 8) | async function setupLocale() {
  function detectCurrentLang (line 67) | function detectCurrentLang() {
  function setAnonymousLocale (line 81) | function setAnonymousLocale(lng: string) {
  function tString (line 89) | function tString(key: string, args?: any, instance = i18next): string {
  type InferResult (line 100) | type InferResult<T> = T extends Record<string, string | number | boolean...
  function t (line 105) | function t<T extends Record<string, any>>(key: string, args?: T | null, ...
  function domT (line 109) | function domT(key: string, args: any, tImpl: typeof i18next.t) {
  function hasTranslation (line 138) | function hasTranslation(key: string) {
  function missingInterpolationHandler (line 142) | function missingInterpolationHandler(key: string, value: any) {
  function isLikeDomContents (line 149) | function isLikeDomContents(value: any): boolean {
  function makeT (line 161) | function makeT(scope: string, instance?: typeof i18next) {

FILE: app/client/lib/log.ts
  type LogMethod (line 8) | type LogMethod = (message: string, ...args: any[]) => void;

FILE: app/client/lib/markdown.ts
  function markdown (line 27) | function markdown(markdownObs: BindableValue<string>, options: { inline?...
  function cssMarkdownSpan (line 36) | function cssMarkdownSpan(
  function inlineMarkdown (line 58) | function inlineMarkdown(markdownObs: BindableValue<string>): DomContents {
  function getMarkdownValue (line 62) | function getMarkdownValue(markdownValue: string, options: { inline?: boo...
  function stripLinks (line 72) | function stripLinks(markdownText: string) {

FILE: app/client/lib/nameUtils.ts
  constant VALID_NAME_REGEXP (line 7) | const VALID_NAME_REGEXP = /^(\w|[^\u0000-\u007F])(\w|[- ./'"()]|[^\u0000...
  function checkName (line 12) | function checkName(name: string): boolean {

FILE: app/client/lib/pausableObs.ts
  type PausableObservable (line 3) | interface PausableObservable<T> extends Observable<T> {
  function createPausableObs (line 15) | function createPausableObs<T>(

FILE: app/client/lib/popupControl.ts
  function popupControl (line 16) | function popupControl(reference: Element, domCreator: IPopupDomCreator, ...

FILE: app/client/lib/popupUtils.ts
  function documentCursor (line 7) | function documentCursor(type: "ns-resize" | "grabbing"): IDisposable {
  function movable (line 28) | function movable<T>(options: {

FILE: app/client/lib/sanitizeUrl.ts
  function sanitizeHttpUrl (line 16) | function sanitizeHttpUrl(url: string): string | null {
  function sanitizeLinkUrl (line 40) | function sanitizeLinkUrl(url: string): string | null {

FILE: app/client/lib/sessionObs.ts
  type SessionObs (line 10) | interface SessionObs<T> extends Observable<T> {
  function createSessionObs (line 36) | function createSessionObs<T>(
  function isNumber (line 65) | function isNumber(t: any): t is number { return typeof t === "number"; }
  function isBoolean (line 66) | function isBoolean(t: any): t is boolean { return typeof t === "boolean"; }
  function isString (line 67) | function isString(t: any): t is string { return typeof t === "string"; }

FILE: app/client/lib/simpleList.ts
  type ISimpleListOpt (line 26) | interface ISimpleListOpt<T, U extends IOption<T> = IOption<T>> {
  class SimpleList (line 32) | class SimpleList<T, U extends IOption<T> = IOption<T>> extends Disposable {
    method constructor (line 39) | constructor(private _ctl: IOp
Copy disabled (too large) Download .json
Condensed preview — 2151 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (21,923K chars).
[
  {
    "path": ".dockerignore",
    "chars": 216,
    "preview": "# explicitly list the files needed by docker.\n*\n!package.json\n!yarn.lock\n!tsconfig-ext.json\n!tsconfig-prod.json\n!tsconfi"
  },
  {
    "path": ".editorconfig",
    "chars": 318,
    "preview": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines wit"
  },
  {
    "path": ".git-blame-ignore-revs",
    "chars": 451,
    "preview": "a7eb4d6e60c50375a835a5300b8e6beebcfb8422\n3a859ee5454d9c2b9600c9d9a3bd859d65d496bf\n9c9c45a6894e80748f0e337d4f164d40b503ca"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 65,
    "preview": "# These are supported funding model platforms\n\ngithub: gristlabs\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/00-bug-issue.yml",
    "chars": 1706,
    "preview": "# Inspired by PeerTube templates:\n# https://github.com/Chocobozzz/PeerTube/blob/3d4d49a23eae71f3ce62cbbd7d93f07336a106b7"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/10-installation-issue.yml",
    "chars": 1156,
    "preview": "# Inspired by PeerTube templates:\n# https://github.com/Chocobozzz/PeerTube/blob/master/.github/ISSUE_TEMPLATE/10-install"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/20-feature-request.yml",
    "chars": 873,
    "preview": "# Inspired by PeerTube templates:\n# https://github.com/Chocobozzz/PeerTube/blob/master/.github/ISSUE_TEMPLATE/30-feature"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 307,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: 🤷💻🤦 Question/Forum\n    url: https://community.getgrist.com/\n    abo"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 944,
    "preview": "## Context\n\n<!-- Please include a summary of the change, with motivation and context -->\n<!-- Bonus: if you are comforta"
  },
  {
    "path": ".github/cla/individual-cla.md",
    "chars": 6181,
    "preview": "# Individual Contributor License Agreement (\"Agreement\"), v1.0\n\n(Based on https://www.apache.org/licenses/icla.pdf by th"
  },
  {
    "path": ".github/cla/signatures.json",
    "chars": 6008,
    "preview": "{\n  \"signedContributors\": [\n    {\n      \"name\": \"jordigh\",\n      \"id\": 260143,\n      \"comment_id\": 3053400623,\n      \"cr"
  },
  {
    "path": ".github/workflows/cla.yml",
    "chars": 1005,
    "preview": "# Workflow body from https://github.com/contributor-assistant/github-action\n\nname: \"CLA Assistant\"\non:\n  issue_comment:\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 3787,
    "preview": "name: Push Docker image\n\non:\n  release:\n    types: [published]\n  # Allows you to run this workflow manually from the Act"
  },
  {
    "path": ".github/workflows/docker_latest.yml",
    "chars": 7172,
    "preview": "name: Push latest Docker image\n\non:\n  push:\n    # Trigger if latest_candidate updates. This is automatically done by ano"
  },
  {
    "path": ".github/workflows/fly-build.yml",
    "chars": 1870,
    "preview": "# fly-deploy will be triggered on completion of this workflow to actually deploy the code to fly.io.\n\nname: fly.io Build"
  },
  {
    "path": ".github/workflows/fly-cleanup.yml",
    "chars": 567,
    "preview": "name: fly.io Cleanup\non:\n  schedule:\n    # Once a day, clean up jobs marked as expired\n    - cron: '50 12 * * *'\n\n  # Al"
  },
  {
    "path": ".github/workflows/fly-deploy.yml",
    "chars": 3099,
    "preview": "# Follow-up of fly-build, with access to secrets for making deployments.\n# This workflow runs in the target repo context"
  },
  {
    "path": ".github/workflows/fly-destroy.yml",
    "chars": 1350,
    "preview": "# This workflow runs in the target repo context, as it is triggered via pull_request_target.\n# It does not, and should n"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 10162,
    "preview": "name: CI\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\n  # Allows running this workflow ma"
  },
  {
    "path": ".github/workflows/self-hosted.yml",
    "chars": 425,
    "preview": "name: Add self-hosting issues to the self-hosting project\n\non:\n  issues:\n    types:\n      - opened\n      - labeled\n\njobs"
  },
  {
    "path": ".github/workflows/translation_keys.yml",
    "chars": 1981,
    "preview": "name: Translation keys\n\non:\n  push:\n    branches: [ main ]\n  workflow_dispatch:\n\npermissions:\n  pull-requests: write\n  c"
  },
  {
    "path": ".gitignore",
    "chars": 1560,
    "preview": "/node_modules/\n/_build/\n/static/*.bundle.js\n/static/*.bundle.js.map\n/static/grist-plugin-api*\n/static/bundle.css\n/static"
  },
  {
    "path": ".nvmrc",
    "chars": 9,
    "preview": "v22.12.0\n"
  },
  {
    "path": ".yarnrc",
    "chars": 112,
    "preview": "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile v1\n\n\nyarn-offline-mirror false\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 452,
    "preview": "# Welcome to the contribution guide for Grist!\n\nYou are eager to contribute to Grist? That's awesome! See below some con"
  },
  {
    "path": "Dockerfile",
    "chars": 7976,
    "preview": "################################################################################\n## The Grist source can be extended. Th"
  },
  {
    "path": "LICENSE.txt",
    "chars": 11351,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "NOTICE.txt",
    "chars": 141,
    "preview": "Grist Software\nCopyright 2014-2022 Grist Labs Inc.\n\nThis product includes software developed at\nGrist Labs Inc. (https:/"
  },
  {
    "path": "README.md",
    "chars": 45865,
    "preview": "# Grist\n\nGrist is a modern relational spreadsheet. It combines the flexibility of a spreadsheet with the robustness of a"
  },
  {
    "path": "SECURITY.md",
    "chars": 678,
    "preview": "# Security Policy\n\n## Supported Versions\n\n| Version  | Supported          |\n| -------  | ------------------ |\n| >= 1.3.2"
  },
  {
    "path": "app/cli.sh",
    "chars": 103,
    "preview": "#!/usr/bin/env bash\n\nNODE_PATH=_build:_build/stubs:_build/ext node _build/app/server/companion.js \"$@\"\n"
  },
  {
    "path": "app/client/DefaultHooks.ts",
    "chars": 563,
    "preview": "import { UrlTweaks } from \"app/common/gristUrls\";\n\nimport { IAttrObj } from \"grainjs\";\n\nexport interface IHooks {\n  ifra"
  },
  {
    "path": "app/client/Hooks.ts",
    "chars": 92,
    "preview": "import { defaultHooks } from \"app/client/DefaultHooks\";\n\nexport const hooks = defaultHooks;\n"
  },
  {
    "path": "app/client/aclui/ACLColumnList.ts",
    "chars": 4013,
    "preview": "/**\n * Implements a widget for showing and editing a list of colIds. It offers a select dropdown to\n * add a new column,"
  },
  {
    "path": "app/client/aclui/ACLFormulaEditor.ts",
    "chars": 5543,
    "preview": "import { setupAceEditorCompletions } from \"app/client/components/AceEditorCompletions\";\nimport { expandAndFilterSuggesti"
  },
  {
    "path": "app/client/aclui/ACLMemoEditor.ts",
    "chars": 995,
    "preview": "import { theme } from \"app/client/ui2018/cssVars\";\n\nimport { dom, DomElementArg, Observable, styled } from \"grainjs\";\n\ne"
  },
  {
    "path": "app/client/aclui/ACLSelect.ts",
    "chars": 1113,
    "preview": "import { theme } from \"app/client/ui2018/cssVars\";\nimport { icon } from \"app/client/ui2018/icons\";\nimport { IOption, sel"
  },
  {
    "path": "app/client/aclui/ACLUsers.ts",
    "chars": 8043,
    "preview": "import { makeT } from \"app/client/lib/localization\";\nimport { DocPageModel } from \"app/client/models/DocPageModel\";\nimpo"
  },
  {
    "path": "app/client/aclui/AccessRules.ts",
    "chars": 96024,
    "preview": "/**\n * UI for managing granular ACLs.\n */\nimport { aclColumnList } from \"app/client/aclui/ACLColumnList\";\nimport { aclFo"
  },
  {
    "path": "app/client/aclui/PermissionsWidget.ts",
    "chars": 6953,
    "preview": "/**\n * Implements a widget showing 3-state boxes for permissions\n * (for Allow / Deny / Pass-Through).\n */\nimport { make"
  },
  {
    "path": "app/client/apiconsole.ts",
    "chars": 16153,
    "preview": "import { loadCssFile, loadScript } from \"app/client/lib/loadScript\";\nimport { makeT } from \"app/client/lib/localization\""
  },
  {
    "path": "app/client/app.css",
    "chars": 5674,
    "preview": "/* global variables */\n@layer grist-base {\n  :root {\n    --color-logo-row:  #F9AE41;\n    --color-logo-col:  #2CB0AF;\n   "
  },
  {
    "path": "app/client/app.js",
    "chars": 2198,
    "preview": "/* global $, window */\n\n// This is the entry point into loading the whole of Grist frontend application. Some extensions"
  },
  {
    "path": "app/client/billingMain.ts",
    "chars": 213,
    "preview": "import { BillingPage } from \"app/client/ui/BillingPage\";\nimport { createAppPage } from \"app/client/ui/createAppPage\";\n\ni"
  },
  {
    "path": "app/client/browserCheck.ts",
    "chars": 2859,
    "preview": "/**\n * Check if browser is a version we are happy with.\n * Introduce any new dependencies very carefully, checking that "
  },
  {
    "path": "app/client/components/AceEditor.css",
    "chars": 2111,
    "preview": ".ace_editor {\n  background-color: var(--grist-theme-ace-editor-bg, white);\n}\n\n.ace_editor .ace_placeholder {\n  font-fami"
  },
  {
    "path": "app/client/components/AceEditor.js",
    "chars": 10278,
    "preview": "var ace = require(\"ace-builds\");\nvar _ = require(\"underscore\");\n// ace-builds also has a minified build (src-min-noconfl"
  },
  {
    "path": "app/client/components/AceEditorCompletions.ts",
    "chars": 15256,
    "preview": "import { ISuggestionWithValue } from \"app/common/ActiveDocAPI\";\nimport { commonUrls } from \"app/common/gristUrls\";\n\nimpo"
  },
  {
    "path": "app/client/components/ActionCounter.ts",
    "chars": 4671,
    "preview": "import * as dispose from \"app/client/lib/dispose\";\nimport { DocData } from \"app/client/models/DocData\";\nimport { Minimal"
  },
  {
    "path": "app/client/components/ActionLog.css",
    "chars": 2769,
    "preview": ".action_log {\n  padding: 1rem;\n  margin: 0;\n}\n\n.action_log_item {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n  font-"
  },
  {
    "path": "app/client/components/ActionLog.ts",
    "chars": 27868,
    "preview": "/**\n * ActionLog manages the list of actions from server and displays them in the side bar.\n */\n\nimport { GristDoc } fro"
  },
  {
    "path": "app/client/components/Banner.ts",
    "chars": 4583,
    "preview": "import { colors, isNarrowScreenObs } from \"app/client/ui2018/cssVars\";\nimport { icon } from \"app/client/ui2018/icons\";\ni"
  },
  {
    "path": "app/client/components/BaseView.ts",
    "chars": 42547,
    "preview": "import { getDefaultColValues } from \"app/client/components/BaseView2\";\nimport { CutCallback } from \"app/client/component"
  },
  {
    "path": "app/client/components/BaseView2.ts",
    "chars": 5957,
    "preview": "/**\n * This file contains logic moved from BaseView.js and ported to TS.\n */\n\nimport { GristDoc } from \"app/client/compo"
  },
  {
    "path": "app/client/components/BehavioralPromptsManager.ts",
    "chars": 7419,
    "preview": "import { showNewsPopup, showTipPopup } from \"app/client/components/modals\";\nimport { logTelemetryEvent } from \"app/clien"
  },
  {
    "path": "app/client/components/CellPosition.ts",
    "chars": 2188,
    "preview": "import BaseRowModel from \"app/client/models/BaseRowModel\";\nimport { DocModel, ViewFieldRec } from \"app/client/models/Doc"
  },
  {
    "path": "app/client/components/CellSelector.ts",
    "chars": 5571,
    "preview": "import { between } from \"app/common/gutil\";\n\nimport { Disposable } from \"grainjs\";\nimport ko from \"knockout\";\n\nimport ty"
  },
  {
    "path": "app/client/components/ChartView.css",
    "chars": 700,
    "preview": ".chart_container {\n  overflow: hidden;\n  position: absolute;\n  height: 100%;\n  width: 100%;\n}\n\n/* Add some spacing betwe"
  },
  {
    "path": "app/client/components/ChartView.ts",
    "chars": 52951,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport { GristDoc } from \"app/client/components/GristDoc\";\nimport"
  },
  {
    "path": "app/client/components/ClientScope.ts",
    "chars": 1557,
    "preview": "import * as dispose from \"app/client/lib/dispose\";\nimport { Storage } from \"app/plugin/StorageAPI\";\nimport { checkers } "
  },
  {
    "path": "app/client/components/Clipboard.css",
    "chars": 275,
    "preview": "/**\n * With some guidance from Lucidchart:\n * https://www.lucidchart.com/techblog/2014/12/02/definitive-guide-copying-pa"
  },
  {
    "path": "app/client/components/Clipboard.ts",
    "chars": 14172,
    "preview": "/**\n * Clipboard component manages the copy/cut/paste events by capturing these events from the browser,\n * managing the"
  },
  {
    "path": "app/client/components/CodeEditorPanel.css",
    "chars": 1137,
    "preview": ".g-code-panel {\n  padding: 10px;\n  overflow: auto;\n}\n\n.g-code-viewer {\n  padding: 2rem 1rem;\n  font-family: monospace;\n "
  },
  {
    "path": "app/client/components/CodeEditorPanel.ts",
    "chars": 2718,
    "preview": "import { GristDoc } from \"app/client/components/GristDoc\";\nimport { makeT } from \"app/client/lib/localization\";\nimport {"
  },
  {
    "path": "app/client/components/ColumnFilters.css",
    "chars": 1722,
    "preview": "/* Hide column menus by default */\n.column_name .g-column-menu-btn {\n  visibility: hidden;\n}\n\n/* Make visible if open or"
  },
  {
    "path": "app/client/components/ColumnTransform.ts",
    "chars": 11662,
    "preview": "/**\n * ColumnTransform is used as a abstract base class for any classes which must build a dom for the\n * purpose of all"
  },
  {
    "path": "app/client/components/Comm.ts",
    "chars": 16965,
    "preview": "/**\n * The Comm object in this module implements communication with the server. We\n * communicate via request-response c"
  },
  {
    "path": "app/client/components/CopySelection.ts",
    "chars": 2185,
    "preview": "import type { ViewFieldRec } from \"app/client/models/entities/ViewFieldRec\";\nimport type { CellValue } from \"app/common/"
  },
  {
    "path": "app/client/components/CoreBanners.ts",
    "chars": 624,
    "preview": "import { ExternalAttachmentBanner } from \"app/client/components/ExternalAttachmentBanner\";\nimport { VersionUpdateBanner "
  },
  {
    "path": "app/client/components/Cursor.ts",
    "chars": 13064,
    "preview": "/**\n * The Cursor module contains functionality related to the cell with the cursor, i.e. a single\n * currently selected"
  },
  {
    "path": "app/client/components/CursorMonitor.ts",
    "chars": 5504,
    "preview": "import { GristDoc } from \"app/client/components/GristDoc\";\nimport { getStorage } from \"app/client/lib/storage\";\nimport {"
  },
  {
    "path": "app/client/components/CustomCalendarView.ts",
    "chars": 356,
    "preview": "import { CustomView, CustomViewSettings } from \"app/client/components/CustomView\";\nimport { AccessLevel } from \"app/comm"
  },
  {
    "path": "app/client/components/CustomView.css",
    "chars": 1067,
    "preview": "/*\n * Ensure the custom view section fits within its allocated area even if it needs to scroll inside\n * of it. This is "
  },
  {
    "path": "app/client/components/CustomView.ts",
    "chars": 17039,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport * as commands from \"app/client/components/commands\";\nimpor"
  },
  {
    "path": "app/client/components/DataTables.ts",
    "chars": 15271,
    "preview": "import * as commands from \"app/client/components/commands\";\nimport { GristDoc } from \"app/client/components/GristDoc\";\ni"
  },
  {
    "path": "app/client/components/DetailView.css",
    "chars": 8544,
    "preview": ".detail_menu_bottom {\n  border-top: 1px solid lightgrey;\n}\n\n/* applies to the record detail container */\n.record-layout-"
  },
  {
    "path": "app/client/components/DetailView.ts",
    "chars": 24090,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport { parsePasteForView } from \"app/client/components/BaseView"
  },
  {
    "path": "app/client/components/DocComm.ts",
    "chars": 9334,
    "preview": "import { Comm } from \"app/client/components/Comm\";\nimport { reportError, reportMessage } from \"app/client/models/errors\""
  },
  {
    "path": "app/client/components/DocumentUsage.ts",
    "chars": 14932,
    "preview": "import { cssBannerLink } from \"app/client/components/Banner\";\nimport { getExternalStorageRecommendation } from \"app/clie"
  },
  {
    "path": "app/client/components/Drafts.ts",
    "chars": 14949,
    "preview": "import { CellPosition, toCursor } from \"app/client/components/CellPosition\";\nimport { GristDoc } from \"app/client/compon"
  },
  {
    "path": "app/client/components/DropdownConditionConfig.ts",
    "chars": 7782,
    "preview": "import { buildDropdownConditionEditor } from \"app/client/components/DropdownConditionEditor\";\nimport { GristDoc } from \""
  },
  {
    "path": "app/client/components/DropdownConditionEditor.ts",
    "chars": 7350,
    "preview": "import * as AceEditor from \"app/client/components/AceEditor\";\nimport { createGroup } from \"app/client/components/command"
  },
  {
    "path": "app/client/components/EditorMonitor.ts",
    "chars": 6070,
    "preview": "import { CellPosition, toCursor } from \"app/client/components/CellPosition\";\nimport { oneTimeListener } from \"app/client"
  },
  {
    "path": "app/client/components/EmbedForm.css",
    "chars": 518,
    "preview": ".embed-form-desc {\n  margin: 10px 0;\n}\n\n.embed-form-basket-id {\n  font-weight: bold;\n  margin-right: 5px;\n}\n\n.embed-form"
  },
  {
    "path": "app/client/components/ExternalAttachmentBanner.ts",
    "chars": 3164,
    "preview": "import { Banner, buildBannerMessage, cssBannerLink } from \"app/client/components/Banner\";\nimport { makeT } from \"app/cli"
  },
  {
    "path": "app/client/components/FieldConfigTab.css",
    "chars": 130,
    "preview": ".formula_button_f {\n  font-size: 1.2rem;\n}\n\n.formula_button_x {\n  font-style: bold;\n  font-size: 0.9rem;\n  line-height: "
  },
  {
    "path": "app/client/components/FormRenderer.ts",
    "chars": 33274,
    "preview": "import * as css from \"app/client/components/FormRendererCss\";\nimport { bindMarkdown } from \"app/client/components/Forms/"
  },
  {
    "path": "app/client/components/FormRendererCss.ts",
    "chars": 9108,
    "preview": "import { colors, mediaXSmall, vars } from \"app/client/ui2018/cssVars\";\nimport { icon } from \"app/client/ui2018/icons\";\ni"
  },
  {
    "path": "app/client/components/Forms/Columns.ts",
    "chars": 8211,
    "preview": "import { FormLayoutNode } from \"app/client/components/FormRenderer\";\nimport { buildEditor } from \"app/client/components/"
  },
  {
    "path": "app/client/components/Forms/Editor.ts",
    "chars": 7794,
    "preview": "import { allCommands } from \"app/client/components/commands\";\nimport { buildMenu } from \"app/client/components/Forms/Men"
  },
  {
    "path": "app/client/components/Forms/Field.ts",
    "chars": 21390,
    "preview": "import { FormLayoutNode, selectPlaceholder, sortChoicesInPlace } from \"app/client/components/FormRenderer\";\nimport { bui"
  },
  {
    "path": "app/client/components/Forms/FormConfig.ts",
    "chars": 6220,
    "preview": "import { fromKoSave } from \"app/client/lib/fromKoSave\";\nimport { makeT } from \"app/client/lib/localization\";\nimport { Vi"
  },
  {
    "path": "app/client/components/Forms/FormView.ts",
    "chars": 34084,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport * as commands from \"app/client/components/commands\";\nimpor"
  },
  {
    "path": "app/client/components/Forms/MappedFieldsConfig.ts",
    "chars": 8955,
    "preview": "import { allCommands } from \"app/client/components/commands\";\nimport { makeT } from \"app/client/lib/localization\";\nimpor"
  },
  {
    "path": "app/client/components/Forms/Menu.ts",
    "chars": 7510,
    "preview": "import { allCommands } from \"app/client/components/commands\";\nimport { FormLayoutNodeType } from \"app/client/components/"
  },
  {
    "path": "app/client/components/Forms/Model.ts",
    "chars": 13268,
    "preview": "import { FormLayoutNode, FormLayoutNodeType } from \"app/client/components/FormRenderer\";\nimport * as elements from \"app/"
  },
  {
    "path": "app/client/components/Forms/Paragraph.ts",
    "chars": 3430,
    "preview": "import { FormLayoutNode } from \"app/client/components/FormRenderer\";\nimport { buildEditor } from \"app/client/components/"
  },
  {
    "path": "app/client/components/Forms/Section.ts",
    "chars": 4604,
    "preview": "import { allCommands } from \"app/client/components/commands\";\nimport { FormLayoutNode } from \"app/client/components/Form"
  },
  {
    "path": "app/client/components/Forms/Submit.ts",
    "chars": 762,
    "preview": "import * as css from \"app/client/components/FormRendererCss\";\nimport { BoxModel } from \"app/client/components/Forms/Mode"
  },
  {
    "path": "app/client/components/Forms/elements.ts",
    "chars": 1179,
    "preview": "import { FormLayoutNode, FormLayoutNodeType } from \"app/client/components/FormRenderer\";\nimport { Columns, Placeholder }"
  },
  {
    "path": "app/client/components/Forms/styles.ts",
    "chars": 18132,
    "preview": "import { textarea } from \"app/client/ui/inputs\";\nimport { sanitizeHTMLIntoDOM } from \"app/client/ui/sanitizeHTML\";\nimpor"
  },
  {
    "path": "app/client/components/FormulaTransform.ts",
    "chars": 1838,
    "preview": "/**\n * FormulaTransform extends ColumnTransform, creating the transform dom in the field config tab\n * used to transform"
  },
  {
    "path": "app/client/components/GridView.css",
    "chars": 11769,
    "preview": ".gridview_data_pane {\n  background-color: var(--grist-theme-table-body-bg, white);\n  position: relative;\n  width: 100%;\n"
  },
  {
    "path": "app/client/components/GridView.ts",
    "chars": 107367,
    "preview": "import BaseView, { ViewOptions } from \"app/client/components/BaseView\";\nimport { parsePasteForView } from \"app/client/co"
  },
  {
    "path": "app/client/components/GristClientSocket.ts",
    "chars": 4867,
    "preview": "import { Socket as EIOSocket } from \"engine.io-client\";\nimport WS from \"ws\";\n\nexport interface GristClientSocketOptions "
  },
  {
    "path": "app/client/components/GristDoc.css",
    "chars": 996,
    "preview": "/* container for main buttons */\n.g-doc-menu-main {\n  flex: 1;\n  -webkit-flex: 1;\n}\n\n.btn.g_toolbar_symbol {\n  font-fami"
  },
  {
    "path": "app/client/components/GristDoc.ts",
    "chars": 87363,
    "preview": "/**\n * GristDoc manages an open Grist document on the client side.\n */\n\nimport { AccessRules } from \"app/client/aclui/Ac"
  },
  {
    "path": "app/client/components/GristWSConnection.ts",
    "chars": 16665,
    "preview": "import { GristClientSocket } from \"app/client/components/GristClientSocket\";\nimport { get as getBrowserGlobals } from \"a"
  },
  {
    "path": "app/client/components/Importer.ts",
    "chars": 71496,
    "preview": "/**\n * Importer manages an import files to Grist tables\n * TODO: hidden tables should be also deleted on page refresh, e"
  },
  {
    "path": "app/client/components/KeyboardFocusHighlighter.ts",
    "chars": 1547,
    "preview": "/**\n * KeyboardFocusHighlighter helps kb users view what interactive elements they have focus on.\n *\n * In an ideal worl"
  },
  {
    "path": "app/client/components/Layout.css",
    "chars": 1744,
    "preview": ".layout_root {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n\n.diff .layout_root {\n  width: 100%;\n}\n\n.layout_ro"
  },
  {
    "path": "app/client/components/Layout.ts",
    "chars": 20345,
    "preview": "/**\n * This module provides the ability to render and edit hierarchical layouts of boxes. Each box may\n * contain a list"
  },
  {
    "path": "app/client/components/LayoutEditor.css",
    "chars": 2150,
    "preview": ".layout_editor_floater {\n  position: absolute;\n  overflow: hidden;\n  pointer-events: none;\n  z-index: 10;\n  -webkit-tran"
  },
  {
    "path": "app/client/components/LayoutEditor.ts",
    "chars": 34260,
    "preview": "/**\n * The LayoutEditor can be attached to a Layout object to allow changing it.\n *\n * Issues:\n * TODO: Hitting ESC whil"
  },
  {
    "path": "app/client/components/LayoutTray.ts",
    "chars": 45108,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport { buildCollapsedSectionDom, buildViewSectionDom } from \"ap"
  },
  {
    "path": "app/client/components/LinkingState.ts",
    "chars": 34890,
    "preview": "import { SequenceNEVER, SequenceNum } from \"app/client/components/Cursor\";\nimport { DataRowModel } from \"app/client/mode"
  },
  {
    "path": "app/client/components/Login.css",
    "chars": 1165,
    "preview": ".login-services {\n  margin: 0 15%;\n}\n\n.login-btns > .kf_elem {\n  flex: 1 1 100%;\n}\n\n.login-spacer {\n  height: 10px;\n}\n\n."
  },
  {
    "path": "app/client/components/ParseOptions.ts",
    "chars": 4788,
    "preview": "import { makeT } from \"app/client/lib/localization\";\nimport { markdown } from \"app/client/lib/markdown\";\nimport { bigBas"
  },
  {
    "path": "app/client/components/PluginScreen.ts",
    "chars": 4376,
    "preview": "import { makeT } from \"app/client/lib/localization\";\nimport { bigBasicButton } from \"app/client/ui2018/buttons\";\nimport "
  },
  {
    "path": "app/client/components/Printing.css",
    "chars": 1637,
    "preview": "@media print {\n  /* Various style overrides needed to print a single section (page widget). */\n\n  .print-hide {\n    disp"
  },
  {
    "path": "app/client/components/Printing.ts",
    "chars": 5931,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport { CustomView } from \"app/client/components/CustomView\";\nim"
  },
  {
    "path": "app/client/components/RawDataPage.ts",
    "chars": 6486,
    "preview": "import { buildViewSectionDom } from \"app/client/components/buildViewSectionDom\";\nimport * as commands from \"app/client/c"
  },
  {
    "path": "app/client/components/RecordCardPopup.ts",
    "chars": 2791,
    "preview": "import { buildViewSectionDom } from \"app/client/components/buildViewSectionDom\";\nimport * as commands from \"app/client/c"
  },
  {
    "path": "app/client/components/RecordLayout.css",
    "chars": 1141,
    "preview": ".g_record_layout_leaf {\n  width: 100%;\n}\n\n.g_record_layout_editing {\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  wi"
  },
  {
    "path": "app/client/components/RecordLayout.js",
    "chars": 15322,
    "preview": "/**\n * Module for displaying a record of user data in a two-dimentional editable layout.\n */\n\n\n// TODO:\n// 1. Consider a"
  },
  {
    "path": "app/client/components/RecordLayoutEditor.js",
    "chars": 5507,
    "preview": "var _ = require(\"underscore\");\nvar BackboneEvents = require(\"backbone\").Events;\n\nvar dispose = require(\"app/client/lib/d"
  },
  {
    "path": "app/client/components/RefSelect.ts",
    "chars": 9490,
    "preview": "import { KoArray } from \"app/client/lib/koArray\";\nimport * as koArray from \"app/client/lib/koArray\";\nimport { makeT } fr"
  },
  {
    "path": "app/client/components/RegionFocusSwitcher.ts",
    "chars": 26956,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport * as commands from \"app/client/components/commands\";\nimpor"
  },
  {
    "path": "app/client/components/SearchBar.css",
    "chars": 1477,
    "preview": ".searchbar-box.grist-navbar-pfx.part-toolbar-group__item {\n  display: flex;\n  width: 15rem;\n  padding: 0;\n  color: grey;"
  },
  {
    "path": "app/client/components/SelectionSummary.ts",
    "chars": 12953,
    "preview": "import { CellSelector, COL, ROW } from \"app/client/components/CellSelector\";\nimport { copyToClipboard } from \"app/client"
  },
  {
    "path": "app/client/components/TypeConversion.ts",
    "chars": 13449,
    "preview": "/**\n * This module contains various logic for converting columns between types. It is used from\n * TypeTransform.js.\n */"
  },
  {
    "path": "app/client/components/TypeTransform.ts",
    "chars": 8403,
    "preview": "/**\n * TypeTransform extends ColumnTransform, creating the transform dom prompt that is shown when the\n * user changes t"
  },
  {
    "path": "app/client/components/UndoStack.ts",
    "chars": 7542,
    "preview": "import { GristDoc } from \"app/client/components/GristDoc\";\nimport * as dispose from \"app/client/lib/dispose\";\nimport { M"
  },
  {
    "path": "app/client/components/UnsavedChanges.ts",
    "chars": 1618,
    "preview": "/**\n * Module to help deal with unsaved changes when closing a page.\n */\nimport { Disposable } from \"grainjs\";\n\n/**\n * C"
  },
  {
    "path": "app/client/components/VersionUpdateBanner.ts",
    "chars": 2759,
    "preview": "import { Banner, buildBannerMessage } from \"app/client/components/Banner\";\nimport { makeT } from \"app/client/lib/localiz"
  },
  {
    "path": "app/client/components/ViewAsBanner.ts",
    "chars": 4417,
    "preview": "import { ACLUsersPopup } from \"app/client/aclui/ACLUsers\";\nimport { Banner } from \"app/client/components/Banner\";\nimport"
  },
  {
    "path": "app/client/components/ViewConfigTab.css",
    "chars": 713,
    "preview": ".view_config_draggable_field {\n  position: relative;\n  margin: .2rem .5rem;\n  padding: .2rem;\n  white-space: nowrap;\n  o"
  },
  {
    "path": "app/client/components/ViewConfigTab.js",
    "chars": 10527,
    "preview": "var _ = require(\"underscore\");\nvar ko = require(\"knockout\");\nvar dispose = require(\"../lib/dispose\");\nvar dom = require("
  },
  {
    "path": "app/client/components/ViewLayout.css",
    "chars": 4723,
    "preview": ".view_leaf {\n  position: relative;\n  flex: 1 1 0px;\n}\n\n.viewsection_buttons {\n  margin-left: 4px;\n  display: flex;\n  ali"
  },
  {
    "path": "app/client/components/ViewLayout.ts",
    "chars": 21778,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport { buildViewSectionDom } from \"app/client/components/buildV"
  },
  {
    "path": "app/client/components/ViewLinker.css",
    "chars": 1698,
    "preview": ".g_record_layout_linking {\n  position: absolute;\n  display: -webkit-flex;\n  display: flex;\n  -webkit-flex-direction: col"
  },
  {
    "path": "app/client/components/ViewPane.ts",
    "chars": 408,
    "preview": "// This module is unused except to group some modules for a webpack bundle.\n// TODO It is a vestige of the old ViewPane."
  },
  {
    "path": "app/client/components/VirtualDoc.ts",
    "chars": 40725,
    "preview": "import { ActionCounter } from \"app/client/components/ActionCounter\";\nimport { ActionLog } from \"app/client/components/Ac"
  },
  {
    "path": "app/client/components/VirtualTable.ts",
    "chars": 15496,
    "preview": "import * as commands from \"app/client/components/commands\";\nimport { ViewLayout, ViewSectionHelper } from \"app/client/co"
  },
  {
    "path": "app/client/components/WidgetFrame.ts",
    "chars": 30533,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport { CommandName } from \"app/client/components/commandList\";\n"
  },
  {
    "path": "app/client/components/buildViewSectionDom.ts",
    "chars": 8413,
    "preview": "import BaseView from \"app/client/components/BaseView\";\nimport { GristDoc } from \"app/client/components/GristDoc\";\nimport"
  },
  {
    "path": "app/client/components/commandList.ts",
    "chars": 19776,
    "preview": "import { makeT } from \"app/client/lib/localization\";\n\nconst t = makeT(\"commandList\");\n\nexport type CommandName =\n  | \"ac"
  },
  {
    "path": "app/client/components/commands.css",
    "chars": 1329,
    "preview": ".shortcut_keys {\n  display: inline-block;\n}\n\n.context-menu-item .shortcut_keys {\n  font-size: 1.2rem;\n}\n\n.shortcut_key_i"
  },
  {
    "path": "app/client/components/commands.ts",
    "chars": 13533,
    "preview": "/**\n * Commands are invoked by the user via keyboard shortcuts or mouse clicks, for example, to move\n * the cursor or to"
  },
  {
    "path": "app/client/components/duplicatePage.ts",
    "chars": 2230,
    "preview": "import { duplicateWidgets } from \"app/client/components/duplicateWidget\";\nimport { GristDoc } from \"app/client/component"
  },
  {
    "path": "app/client/components/duplicateWidget.ts",
    "chars": 14184,
    "preview": "import { cleanFormLayoutSpec } from \"app/client/components/FormRenderer\";\nimport { GristDoc } from \"app/client/component"
  },
  {
    "path": "app/client/components/modals.ts",
    "chars": 12980,
    "preview": "import * as commands from \"app/client/components/commands\";\nimport { GristDoc } from \"app/client/components/GristDoc\";\ni"
  },
  {
    "path": "app/client/components/viewCommon.css",
    "chars": 10275,
    "preview": "/*\n  record class is used for grid view header and rows\n */\n.record {\n  display: -webkit-flex;\n  display: flex;\n  positi"
  },
  {
    "path": "app/client/components/viewCommon.js",
    "chars": 2696,
    "preview": "/* global $ */\n\nvar koDom = require(\"../lib/koDom\");\n\n/**\n * This adds `.isFlex` option to JQuery's $.ui.resizable to ma"
  },
  {
    "path": "app/client/declarations.d.ts",
    "chars": 11100,
    "preview": "declare module \"app/client/components/AceEditor\";\ndeclare module \"app/client/components/CodeEditorPanel\";\ndeclare module"
  },
  {
    "path": "app/client/errorMain.ts",
    "chars": 172,
    "preview": "import { createAppPage } from \"app/client/ui/createAppPage\";\nimport { createErrPage } from \"app/client/ui/errorPages\";\n\n"
  },
  {
    "path": "app/client/exposeModulesForTests.js",
    "chars": 389,
    "preview": "/* global window */\n\n// These modules are exposed for the sake of browser tests.\nObject.assign(window.exposedModules, {\n"
  },
  {
    "path": "app/client/formMain.ts",
    "chars": 281,
    "preview": "import { createPage } from \"app/client/ui/createPage\";\nimport { FormPage } from \"app/client/ui/FormPage\";\n\nimport { dom "
  },
  {
    "path": "app/client/lib/ACIndex.ts",
    "chars": 12194,
    "preview": "/**\n * A search index for auto-complete suggestions.\n *\n * This implementation indexes words, and suggests items based o"
  },
  {
    "path": "app/client/lib/ACSelect.ts",
    "chars": 4664,
    "preview": "import { ACIndex, ACItem, buildHighlightedDom } from \"app/client/lib/ACIndex\";\nimport { Autocomplete, IAutocompleteOptio"
  },
  {
    "path": "app/client/lib/ACUserManager.ts",
    "chars": 6190,
    "preview": "import { ACIndex, ACItem, ACResults, buildHighlightedDom, normalizeText } from \"app/client/lib/ACIndex\";\nimport { cssSel"
  },
  {
    "path": "app/client/lib/BoxSpec.ts",
    "chars": 2234,
    "preview": "import { Layout } from \"app/client/components/Layout\";\n\nimport { dom } from \"grainjs\";\nimport * as _ from \"underscore\";\n"
  },
  {
    "path": "app/client/lib/CellDiffTool.ts",
    "chars": 3767,
    "preview": "import { isVersions } from \"app/common/gristTypes\";\nimport { BaseFormatter } from \"app/common/ValueFormatter\";\nimport { "
  },
  {
    "path": "app/client/lib/CustomSectionElement.ts",
    "chars": 1630,
    "preview": "import { SafeBrowser, ViewProcess } from \"app/client/lib/SafeBrowser\";\nimport { PluginInstance } from \"app/common/Plugin"
  },
  {
    "path": "app/client/lib/Delay.ts",
    "chars": 2760,
    "preview": "/**\n * A little class to make it easier to work with setTimeout/clearTimeout when it may need to get\n * cancelled or res"
  },
  {
    "path": "app/client/lib/DocPluginManager.ts",
    "chars": 2757,
    "preview": "import { ClientScope } from \"app/client/components/ClientScope\";\nimport { SafeBrowser } from \"app/client/lib/SafeBrowser"
  },
  {
    "path": "app/client/lib/DocSchemaImport.ts",
    "chars": 369,
    "preview": "import { tablesToSchema } from \"app/common/DocSchemaImport\";\nimport { ExistingDocSchema } from \"app/common/DocSchemaImpo"
  },
  {
    "path": "app/client/lib/FocusLayer.ts",
    "chars": 8549,
    "preview": "/**\n * FocusLayer addresses the issue of where focus goes \"by default\". In most of Grist operation,\n * the focus is on t"
  },
  {
    "path": "app/client/lib/GristWindow.ts",
    "chars": 1380,
    "preview": "/**\n * Some client-side code sets global properties (on the global Window object). This isn't a\n * great practice, and s"
  },
  {
    "path": "app/client/lib/HomePluginManager.ts",
    "chars": 2463,
    "preview": "import { ClientScope } from \"app/client/components/ClientScope\";\nimport { SafeBrowser } from \"app/client/lib/SafeBrowser"
  },
  {
    "path": "app/client/lib/ImportSourceElement.ts",
    "chars": 1364,
    "preview": "import { PluginInstance } from \"app/common/PluginInstance\";\nimport { InternalImportSourceAPI } from \"app/plugin/Internal"
  },
  {
    "path": "app/client/lib/Mousetrap.js",
    "chars": 2856,
    "preview": "/**\n * This file adds some includes tweaks to the behavior of Mousetrap.js, the keyboard bindings\n * library. It exports"
  },
  {
    "path": "app/client/lib/MultiUserManager.ts",
    "chars": 5052,
    "preview": "import { IOrgMemberSelectOption, UserManagerModel } from \"app/client/models/UserManagerModel\";\nimport { textarea } from "
  },
  {
    "path": "app/client/lib/ObservableMap.js",
    "chars": 4261,
    "preview": "var ko      = require(\"knockout\");\n\nvar dispose = require(\"./dispose\");\n\n/**\n * ObservableMap provides a structure to ke"
  },
  {
    "path": "app/client/lib/ObservableSet.js",
    "chars": 2057,
    "preview": "var _ = require(\"underscore\");\nvar ko = require(\"knockout\");\nvar dispose = require(\"./dispose\");\n\n/**\n * An ObservableSe"
  },
  {
    "path": "app/client/lib/ReferenceUtils.ts",
    "chars": 5618,
    "preview": "import { GristDoc } from \"app/client/components/GristDoc\";\nimport { ACIndex, ACResults } from \"app/client/lib/ACIndex\";\n"
  },
  {
    "path": "app/client/lib/SafeBrowser.ts",
    "chars": 15914,
    "preview": "/**\n * The SafeBrowser component implementation is responsible for executing the safeBrowser component\n * of a plugin.\n "
  },
  {
    "path": "app/client/lib/SafeBrowserProcess.css",
    "chars": 164,
    "preview": ".plugin_instance_fullscreen {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n  z-index:9999;\n"
  },
  {
    "path": "app/client/lib/Signal.ts",
    "chars": 6021,
    "preview": "import { DisposableWithEvents } from \"app/common/DisposableWithEvents\";\n\nimport { Disposable, IDisposable, IDisposableOw"
  },
  {
    "path": "app/client/lib/Suggestions.ts",
    "chars": 3179,
    "preview": "/**\n * This is a helper for producing autocomplete suggestions (aka completions) for the ACE code\n * editor. In particul"
  },
  {
    "path": "app/client/lib/TokenField.ts",
    "chars": 28520,
    "preview": "/**\n * A full-featured implementation of tokenfield (aka \"pillbox\", \"tag list\", etc).\n *\n * Supported features:\n * - Eac"
  },
  {
    "path": "app/client/lib/UrlState.ts",
    "chars": 7980,
    "preview": "/**\n * Generic support of observable state represented by the current page's URL. The state is\n * initialized on first u"
  },
  {
    "path": "app/client/lib/Validator.ts",
    "chars": 3860,
    "preview": "import { theme } from \"app/client/ui2018/cssVars\";\n\nimport { Disposable, dom, Observable, styled } from \"grainjs\";\n\n/**\n"
  },
  {
    "path": "app/client/lib/airtable/AirtableImportUI.ts",
    "chars": 32155,
    "preview": "import {\n  AirtableImportDestination,\n  AirtableImportResult,\n  applyAirtableImportSchemaAndImportData, ExistingDoc, New"
  },
  {
    "path": "app/client/lib/airtable/AirtableImporter.ts",
    "chars": 5910,
    "preview": "import { getExistingDocSchema } from \"app/client/lib/DocSchemaImport\";\nimport { makeT } from \"app/client/lib/localizatio"
  },
  {
    "path": "app/client/lib/airtable/startDocAirtableImport.ts",
    "chars": 1769,
    "preview": "import { GristDoc } from \"app/client/components/GristDoc\";\nimport { loadAirtableImportUI } from \"app/client/lib/imports\""
  },
  {
    "path": "app/client/lib/airtable/startHomeAirtableImport.ts",
    "chars": 1548,
    "preview": "import { loadAirtableImportUI } from \"app/client/lib/imports\";\nimport { makeT } from \"app/client/lib/localization\";\nimpo"
  },
  {
    "path": "app/client/lib/autocomplete.ts",
    "chars": 10161,
    "preview": "/**\n * Implements an autocomplete dropdown.\n */\nimport { ACItem, ACResults, HighlightFunc } from \"app/client/lib/ACIndex"
  },
  {
    "path": "app/client/lib/browserGlobals.ts",
    "chars": 2064,
    "preview": "/**\n * Module that allows client-side code to use browser globals (such as `document` or `Node`) in a\n * way that allows"
  },
  {
    "path": "app/client/lib/browserInfo.ts",
    "chars": 1283,
    "preview": "import * as Bowser from \"bowser\"; // TypeScript\n\nlet parser: Bowser.Parser.Parser | undefined;\n\nfunction getParser() {\n "
  },
  {
    "path": "app/client/lib/chartUtil.ts",
    "chars": 3541,
    "preview": "import { typedCompare } from \"app/common/SortFunc\";\nimport { decodeObject } from \"app/plugin/objtypes\";\n\nimport flatten "
  },
  {
    "path": "app/client/lib/clipboardUtils.ts",
    "chars": 2546,
    "preview": "import { getBrowserGlobals } from \"app/client/lib/browserGlobals\";\n\nconst G = getBrowserGlobals(\"document\", \"window\");\n\n"
  },
  {
    "path": "app/client/lib/dblclick.ts",
    "chars": 2226,
    "preview": "import { dom, EventCB } from \"grainjs\";\n\nconst DOUBLE_TAP_INTERVAL_MS = 500;\n\n/**\n * Helper to handle 'dblclick' events "
  },
  {
    "path": "app/client/lib/dispose.d.ts",
    "chars": 558,
    "preview": "// TODO: add remaining Disposable method\nexport abstract class Disposable {\n  public static create<T extends new (...arg"
  },
  {
    "path": "app/client/lib/dispose.js",
    "chars": 12930,
    "preview": "/**\n * dispose.js provides tools to components that needs to dispose of resources, such as\n * destroy DOM, and unsubscri"
  },
  {
    "path": "app/client/lib/dom.js",
    "chars": 15719,
    "preview": "// Builds a DOM tree or document fragment, easily.\n//\n// Usage:\n//  dom('a#link.c1.c2', {href:url}, 'Hello ', dom('span'"
  },
  {
    "path": "app/client/lib/domAsync.ts",
    "chars": 1227,
    "preview": "import { reportError } from \"app/client/models/errors\";\n\nimport { DomContents, onDisposeElem, replaceContent } from \"gra"
  },
  {
    "path": "app/client/lib/domUtils.ts",
    "chars": 5063,
    "preview": "import { useBindable } from \"app/common/gutil\";\n\nimport { BindableValue, Computed, dom, EventCB, IDisposable, IDisposabl"
  },
  {
    "path": "app/client/lib/download.js",
    "chars": 1083,
    "preview": "const G = require(\"../lib/browserGlobals\").get(\"document\");\nconst dom = require(\"../lib/dom\");\n\n/**\n * Note about testin"
  },
  {
    "path": "app/client/lib/formUtils.ts",
    "chars": 4576,
    "preview": "import { reportError } from \"app/client/models/errors\";\nimport { ApiError } from \"app/common/ApiError\";\nimport { BaseAPI"
  }
]

// ... and 1951 more files (download for full content)

About this extraction

This page contains the full source code of the gristlabs/grist-core GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 2151 files (19.8 MB), approximately 5.3M tokens, and a symbol index with 15508 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!