Full Code of kaustubh-karkare/glados for AI

master b68436687d0a cached
199 files
537.6 KB
120.2k tokens
841 symbols
1 requests
Download .txt
Showing preview only (588K chars total). Download the full file or copy to clipboard to get everything.
Repository: kaustubh-karkare/glados
Branch: master
Commit: b68436687d0a
Files: 199
Total size: 537.6 KB

Directory structure:
gitextract_nsqvf378/

├── .gitignore
├── .husky/
│   └── pre-commit
├── LICENSE
├── README.md
├── config/
│   ├── babel.config.js
│   ├── demo.glados.json
│   ├── eslint.config.js
│   ├── example.glados.json
│   ├── jest.config.js
│   └── webpack.config.js
├── package.json
└── src/
    ├── README.md
    ├── client/
    │   ├── Application/
    │   │   ├── Application.js
    │   │   ├── BackupSection.js
    │   │   ├── CreditsSection.js
    │   │   ├── DetailsSection.css
    │   │   ├── DetailsSection.js
    │   │   ├── FavoritesSection.js
    │   │   ├── IndexSection.css
    │   │   ├── IndexSection.js
    │   │   ├── TabSection.js
    │   │   ├── URLState.js
    │   │   └── index.js
    │   ├── Bootstrap/
    │   │   ├── InputGroup.css
    │   │   ├── Modal.css
    │   │   ├── Popover.css
    │   │   └── index.js
    │   ├── Common/
    │   │   ├── AddLinkPlugin.js
    │   │   ├── AsyncSelector.js
    │   │   ├── BulletList/
    │   │   │   ├── BulletList.css
    │   │   │   ├── BulletList.js
    │   │   │   ├── BulletListIcon.js
    │   │   │   ├── BulletListItem.js
    │   │   │   ├── BulletListLine.js
    │   │   │   ├── BulletListPager.js
    │   │   │   ├── BulletListTitle.js
    │   │   │   └── index.js
    │   │   ├── ConfirmModal.js
    │   │   ├── Coordinator.js
    │   │   ├── DataLoader.js
    │   │   ├── DateContext.js
    │   │   ├── DatePicker.js
    │   │   ├── DateRangePicker.js
    │   │   ├── Dropdown.css
    │   │   ├── Dropdown.js
    │   │   ├── EditorModal.js
    │   │   ├── EnumSelectorSection.js
    │   │   ├── ErrorModal.js
    │   │   ├── Highlightable.css
    │   │   ├── Highlightable.js
    │   │   ├── Icon.css
    │   │   ├── Icon.js
    │   │   ├── InfoModal.js
    │   │   ├── InputLine.css
    │   │   ├── InputLine.js
    │   │   ├── LeftRight.js
    │   │   ├── Link.js
    │   │   ├── ModalStack.js
    │   │   ├── Plugins.js
    │   │   ├── PopoverElement.js
    │   │   ├── ScrollableSection.css
    │   │   ├── ScrollableSection.js
    │   │   ├── Selector.js
    │   │   ├── SettingsContext.js
    │   │   ├── SidebarSection.css
    │   │   ├── SidebarSection.js
    │   │   ├── SortableList.css
    │   │   ├── SortableList.js
    │   │   ├── StandardIcons.js
    │   │   ├── TextEditor.css
    │   │   ├── TextEditor.js
    │   │   ├── TextInput.js
    │   │   ├── TooltipElement.js
    │   │   ├── TypeaheadInput.js
    │   │   ├── TypeaheadOptions.js
    │   │   ├── TypeaheadSelector.css
    │   │   ├── TypeaheadSelector.js
    │   │   ├── URLManager.js
    │   │   ├── Utils.js
    │   │   └── index.js
    │   ├── Graphs/
    │   │   ├── GraphLineChart.js
    │   │   ├── GraphSection.css
    │   │   ├── GraphSection.js
    │   │   ├── GraphSectionData.js
    │   │   ├── GraphSectionOptions.js
    │   │   ├── GraphTooltip.js
    │   │   └── index.js
    │   ├── LogEvent/
    │   │   ├── LogEventAdder.js
    │   │   ├── LogEventDetailsHeader.js
    │   │   ├── LogEventEditor.js
    │   │   ├── LogEventList.js
    │   │   ├── LogEventOptions.js
    │   │   ├── LogEventSearch.js
    │   │   └── index.js
    │   ├── LogKey/
    │   │   ├── LogKeyEditor.js
    │   │   ├── LogKeyListEditor.js
    │   │   ├── LogValueEditor.js
    │   │   ├── LogValueListEditor.js
    │   │   └── index.js
    │   ├── LogStructure/
    │   │   ├── LogStructureDetailsHeader.js
    │   │   ├── LogStructureEditor.js
    │   │   ├── LogStructureFrequencyEditor.js
    │   │   ├── LogStructureGroupEditor.js
    │   │   ├── LogStructureGroupList.js
    │   │   ├── LogStructureList.js
    │   │   ├── LogStructureOptions.js
    │   │   ├── LogStructureSearch.js
    │   │   └── index.js
    │   ├── LogTopic/
    │   │   ├── LogTopicDetailsHeader.js
    │   │   ├── LogTopicEditor.js
    │   │   ├── LogTopicList.js
    │   │   ├── LogTopicOptions.js
    │   │   ├── LogTopicSearch.js
    │   │   └── index.js
    │   ├── Reminders/
    │   │   ├── ReminderItem.js
    │   │   ├── ReminderList.js
    │   │   ├── ReminderSidebar.js
    │   │   └── index.js
    │   ├── Settings/
    │   │   ├── SettingsEditor.js
    │   │   ├── SettingsModal.js
    │   │   ├── SettingsSection.js
    │   │   └── index.js
    │   ├── __tests__/
    │   │   └── Colors.test.js
    │   ├── index.css
    │   ├── index.html
    │   ├── index.js
    │   └── prop-types.js
    ├── common/
    │   ├── AsyncUtils.js
    │   ├── DateUtils.js
    │   ├── RichTextUtils.js
    │   ├── SocketRPC.js
    │   ├── __tests__/
    │   │   └── RichTextUtils.test.js
    │   ├── data_types/
    │   │   ├── LogEvent.js
    │   │   ├── LogKey.js
    │   │   ├── LogStructure.js
    │   │   ├── LogStructureFrequency.js
    │   │   ├── LogStructureGroup.js
    │   │   ├── LogTopic.js
    │   │   ├── __tests__/
    │   │   │   └── LogStructureFrequency.test.js
    │   │   ├── api.js
    │   │   ├── base.js
    │   │   ├── enum.js
    │   │   ├── index.js
    │   │   ├── utils.js
    │   │   └── validation.js
    │   └── polyfill.js
    ├── demo/
    │   ├── components/
    │   │   ├── Application.js
    │   │   ├── BaseWrapper.js
    │   │   ├── BulletList.js
    │   │   ├── DetailsSection.js
    │   │   ├── IndexSection.js
    │   │   ├── Inputs.js
    │   │   ├── ModalDialog.js
    │   │   ├── ReminderItem.js
    │   │   ├── SidebarSection.js
    │   │   └── index.js
    │   ├── index.js
    │   ├── lessons/
    │   │   ├── 001-events.js
    │   │   ├── 002-topics.js
    │   │   ├── 003-structures.js
    │   │   ├── 004-reminders.js
    │   │   └── 005-graphs.js
    │   ├── lessons.js
    │   └── process.js
    ├── plugins/
    │   ├── README.md
    │   └── kaustubh/
    │       ├── custom.actions.js
    │       ├── long_term_goals/
    │       │   ├── LongTermGoalGraph.js
    │       │   ├── LongTermGoalsSettings.js
    │       │   └── client.js
    │       ├── more_event_lists/
    │       │   ├── MoreEventListsSettings.js
    │       │   └── client.js
    │       ├── time_sections/
    │       │   ├── TimeSection.js
    │       │   ├── TimeSectionSettings.js
    │       │   └── client.js
    │       ├── topic_reminder_sections/
    │       │   ├── TopicRemindersSection.js
    │       │   ├── TopicRemindersSectionSettings.js
    │       │   ├── actions.js
    │       │   └── client.js
    │       └── topic_sections/
    │           ├── TopicSection.js
    │           ├── TopicSectionSettings.js
    │           └── client.js
    └── server/
        ├── __tests__/
        │   └── Config.test.js
        ├── actions/
        │   ├── __tests__/
        │   │   ├── Backup.test.js
        │   │   ├── Database.test.js
        │   │   ├── LogEvent.test.js
        │   │   ├── LogStructure.test.js
        │   │   ├── LogTopic.test.js
        │   │   ├── Reminders.test.js
        │   │   └── TestUtils.js
        │   ├── backup.js
        │   ├── data_types.js
        │   ├── database.js
        │   ├── reminders.js
        │   ├── settings.js
        │   └── suggestions.js
        ├── actions.js
        ├── database.js
        ├── index.js
        └── models.js

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

================================================
FILE: .gitignore
================================================
.DS_Store
/config.json
data
dist
node_modules


================================================
FILE: .husky/pre-commit
================================================
yarn run lint && yarn run test


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) Kaustubh Karkare

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
## Generic Life Activity Data Organization System (GLADOS)

https://user-images.githubusercontent.com/1102450/147822871-ca69bedc-ed20-45aa-88de-e02c66bb92f0.mp4

If the above video does not work, you can also watch it here: https://www.youtube.com/watch?v=xd3JJi8zSk4

### Rationale
* Over the past decade, I have tried using various todo-list apps, but none of them really worked out for me: the motivation never seemed to last beyond a few days. But when I encountered the idea of an anti-todo/done-list in some blog post (maybe [this one](https://www.fastcompany.com/3034785/why-an-anti-to-do-list-might-be-the-secret-to-productivity)?) I was fascinated enough to give it a try, and it actually proved valuable!
* Additionally, back then, it was fairly easy to measure my productivity at work in terms of the amount of code generated, so I would only make notes about important/memorable events. But in the last couple of years, as my job has evolved, that metric is no longer a useful proxy for my effectiveness. This transition strongly correlated with an increasing reliance on these done-lists to feel satisfied at the end of the day.
* I was using Evernote to manage these notes/lists for a few years, and once I had a good understanding of how I like to use the tool, I found myself wishing for the ability to add more structure to the data being generated, so that I can do more interesting things with it, like building custom visualizations and graphs.
* Looking at the options available online, I did not find anything that did everything I was hoping for, and more importantly, it hurt my pride as a Software Engineer to pay for something I knew I can build. I also did not like the idea of relying on an external product that might go out of business at some point in the future: having complete control over my data was a major design goal. As a result, this tool might not be suited to a larger audience, but it definitely works for me! :)

### Warning!

* Since it is primarily designed for an audience of one, this tool is continuously being modified as I find new ways to improve my workflow. It most definitely is NOT perfect, containing edge cases that I have not yet encountered or fixed. But for what it is worth, I have been using it almost daily since July 2020 without any significant issues.

### Installation

```
git clone https://github.com/kaustubh-karkare/glados
cd glados
cp config/example.glados.json config.json
mkdir data
yarn install
yarn run build
yarn run database-reset
```

* The default `config.json` file specifies the `data` subdirectory as the location of the SQLite database and the backups.
* I personally made `data` a symlink to another directory that synced to my [Dropbox](https://www.dropbox.com/).
* You can theoretically change the config to use whatever storage you want, as long as it is compatible with [Sequelize](https://sequelize.org/).
* And once you're ready,

```
yarn run server
```

### Demo

* In order to show off what I have built, I used to manually create videos by recording my screen as I performed a predetermined set of actions. This was obviously very fragile and involved multiple attempts until I finally made no mistakes.
* I got annoyed at this process, and so automated the whole thing using [Selenium Webdriver](https://www.selenium.dev/selenium/docs/api/javascript/index.html) to perform those actions and [ffmpeg](https://www.ffmpeg.org/) to record that part of the screen.

```
yarn run demo
```

* You can see the result at the top of this README file.
* An auxiliary benefit here is that this functionality can be used as an E2E test for the client code.

### Backups

```
yarn run backup-save  # Can also be done via the right-sidebar in the UI.
yarn run backup-load  # This involves a database reset, so be careful!
```

* Backup files are created by loading the entire database into memory and then writing that as a JSON file (less than 10MB for data generated over a full year, uncompressed).
* This makes it very easy to apply transformations on the entire database when needed. Eg - the database schema has been updated, or if you just want to change how you organize things.
* These are also useful if data needs to be moved from one storage to another.

### Community

* https://www.reddit.com/r/glados_app/
* Hacker News: [2022-01-01](https://news.ycombinator.com/item?id=29756591).


================================================
FILE: config/babel.config.js
================================================
module.exports = {
    plugins: [
        '@babel/plugin-proposal-class-properties',
        '@babel/plugin-transform-runtime',
    ],
    presets: [
        '@babel/preset-env',
        '@babel/preset-react',
    ],
    compact: false,
    sourceType: 'unambiguous',
};


================================================
FILE: config/demo.glados.json
================================================
{
    "lock_name": "glados-demo",
    "database": {
        "dialect": "sqlite",
        "storage": "dist/demo/test.sqlite",
        "logging": false
    },
    "backup": {
        "location": "dist/demo",
        "save_interval_ms": null
    },
    "server": {
        "host": "localhost",
        "port": 8081
    }
}


================================================
FILE: config/eslint.config.js
================================================
module.exports = {
    env: {
        browser: true,
        es6: true,
        jest: true,
        node: true,
    },
    extends: [
        'plugin:react/recommended',
        'airbnb',
    ],
    globals: {
        Atomics: 'readonly',
        SharedArrayBuffer: 'readonly',
    },
    parser: '@typescript-eslint/parser',
    parserOptions: {
        ecmaFeatures: {
            jsx: true,
        },
        ecmaVersion: 11,
        sourceType: 'module',
    },
    plugins: [
        'react',
        'simple-import-sort',
    ],
    settings: {
        react: {
            version: '16.13.1',
        },
    },
    rules: {
        indent: ['error', 4],
        'import/no-cycle': [0],
        // Unable to resolve path to module 'react'
        'import/no-unresolved': [0],
        // Need to add role attribute for accessibility on HTML elements.
        'jsx-a11y/no-static-element-interactions': [0],
        'jsx-a11y/click-events-have-key-events': [0],
        'jsx-a11y/mouse-events-have-key-events': [0],
        'jsx-a11y/anchor-is-valid': [0],
        'jsx-a11y/no-noninteractive-tabindex': [0],
        'no-param-reassign': [0],
        'no-underscore-dangle': [0, 'allowAfterThis'],
        'no-unused-vars': ['error', { args: 'none', varsIgnorePattern: '^_' }],
        'react/jsx-indent': ['error', 4],
        'react/jsx-indent-props': ['error', 4],
        'react/destructuring-assignment': [0],
        'react/jsx-filename-extension': [0],
        'react/jsx-props-no-spreading': [0],
        'react/no-unused-class-component-methods': [0],
        // Otherwise, every non-required propType would need defaultValue.
        'react/require-default-props': [0],
        'no-restricted-exports': [0],
        'simple-import-sort/imports': 'error',
    },
};


================================================
FILE: config/example.glados.json
================================================
{
    "database": {
        "dialect": "sqlite",
        "storage": "data/test.sqlite",
        "logging": false
    },
    "backup": {
        "location": "data",
        "save_interval_ms": null
    },
    "server": {
        "host": "localhost",
        "port": 8080
    }
}


================================================
FILE: config/jest.config.js
================================================
const path = require('path');

module.exports = {
    rootDir: '..',
    roots: ['src'],
    testRegex: 'test.js',
    transform: {
        '\\.js$': ['babel-jest', { configFile: path.join(__dirname, 'babel.config.js') }],
    },
};


================================================
FILE: config/webpack.config.js
================================================
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const nodeExternals = require('webpack-node-externals');

const path = require('path');

function fromProjectRoot(relativePath) {
    return path.resolve(__dirname, '..', relativePath);
}

function getJSModuleRule() {
    return {
        test: /\.(js|ts)$/,
        use: [
            {
                loader: 'babel-loader',
                options: {
                    configFile: path.join(__dirname, 'babel.config.js'),
                },
            },
        ],
        exclude: /node_modules/,
    };
}

function getStats() {
    return {
        assets: true, // Show generated bundles.
        builtAt: true, // The one signal I actually want.
        children: false,
        entrypoints: false,
        hash: false,
        modules: false, // Show all the modules that are part of this package.
        timings: false,
        version: false,
    };
}

function getClientSideBundle(entryPoint, outputFileName) {
    return {
        mode: 'development',
        entry: fromProjectRoot(entryPoint),
        output: {
            path: fromProjectRoot('dist'),
            filename: outputFileName,
        },
        devServer: {
            hot: true,
        },
        resolve: {
            extensions: ['.js', '.css'],
            fallback: {
                assert: require.resolve('assert'),
                util: require.resolve('util'),
            },
        },
        module: {
            rules: [
                getJSModuleRule(),
                {
                    test: /\.css$/,
                    use: [
                        MiniCssExtractPlugin.loader,
                        // The css-loader interprets @import and url()
                        // like import/require() and will resolve them.
                        'css-loader',
                    ],
                },
            ],
        },
        plugins: [
            new webpack.ProvidePlugin({
                process: 'process/browser',
            }),
            new MiniCssExtractPlugin({
                filename: 'index.css',
            }),
            new HtmlWebpackPlugin({
                template: fromProjectRoot('src/client/index.html'),
                favicon: 'src/client/index.ico',
            }),
        ],
        stats: getStats(),
    };
}

function getServerSideBundle(entryPoint, outputFileName) {
    return {
        mode: 'development',
        entry: fromProjectRoot(entryPoint),
        output: {
            path: fromProjectRoot('dist'),
            filename: outputFileName,
        },
        devServer: {
            hot: true,
        },
        resolve: {
            extensions: ['.js'],
        },
        module: {
            rules: [
                getJSModuleRule(),
            ],
        },
        // https://medium.com/tomincode/hiding-critical-dependency-warnings-from-webpack-c76ccdb1f6c1
        plugins: [
            new webpack.ContextReplacementPlugin(
                /src\/server/,
                (data) => {
                    // The following error is expected in actions.js to support Jest.
                    //     Critical dependency: require function is used in a way in
                    //     which dependencies cannot be statically extracted.
                    data.dependencies.forEach((dependency) => {
                        delete dependency.critical;
                    });
                    return data;
                },
            ),
        ],
        stats: getStats(),
        // https://www.npmjs.com/package/webpack-node-externals
        target: 'node',
        externals: [nodeExternals()],
    };
}

module.exports = [
    getClientSideBundle('src/client/index.js', 'client.js'),
    getServerSideBundle('src/server/index.js', 'server.js'),
    getServerSideBundle('src/demo/index.js', 'demo.js'),
];


================================================
FILE: package.json
================================================
{
  "name": "glados",
  "version": "1.0.0",
  "description": "Generic Life Activity Data Organization System",
  "private": true,
  "scripts": {
    "build": "webpack --config ./config/webpack.config.js",
    "demo": "node ./dist/demo.js",
    "server": "node ./dist/server.js",
    "server-watch": "nodemon --watch ./dist/server.js --exec node ./dist/server.js",
    "database-reset": "yarn run server -a database-reset",
    "backup-save": "yarn run server -a backup-save",
    "backup-load": "yarn run server -a backup-load",
    "lint": "eslint -c ./config/eslint.config.js --ext .js,.jsx --fix src",
    "test": "jest --config ./config/jest.config.js --no-watchman",
    "todo": "grep -nir '// TODO' src",
    "kill": "ps -A | grep \"bin/node ./dist\" | grep -v grep | awk '{ print \"kill -9\", $1 }' | zsh"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/kaustubh-karkare/glados.git"
  },
  "keywords": [],
  "author": "Kaustubh Karkare",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/kaustubh-karkare/glados/issues"
  },
  "homepage": "https://github.com/kaustubh-karkare/glados#readme",
  "dependencies": {
    "array-move": "^2.2.2",
    "assert": "^2.0.0",
    "bootstrap": "^4.5.0",
    "classnames": "^2.2.6",
    "date-fns": "^2.15.0",
    "date-fns-timezone": "^0.1.4",
    "deep-equal": "^2.0.3",
    "deepcopy": "^2.1.0",
    "draft-js": "^0.11.5",
    "draft-js-markdown-shortcuts-plugin": "^0.6.1",
    "draft-js-mention-plugin": "^3.1.5",
    "draft-js-plugins-editor": "^3.0.0",
    "express": "^5.0.0",
    "markdown-draft-js": "^2.2.1",
    "process": "^0.11.10",
    "prop-types": "^15.7.2",
    "query-string": "^6.13.1",
    "react": "^16.13.1",
    "react-bootstrap": "^1.0.1",
    "react-bootstrap-typeahead": "^5.0.0-rc.1",
    "react-date-range": "^1.0.3",
    "react-datepicker": "^3.0.0",
    "react-dom": "^16.13.1",
    "react-icons": "^3.10.0",
    "react-sortable-hoc": "^1.11.0",
    "recharts": "^2.0.9",
    "selenium-webdriver": "^4.20.0",
    "sequelize": "^6.35.0",
    "single-instance": "^0.0.1",
    "socket.io": "^4.7.0",
    "socket.io-client": "^4.7.0",
    "sqlite3": "^5.1.7",
    "timezone-support": "^2.0.2",
    "toposort": "^2.0.2",
    "util": "^0.12.5",
    "yargs": "^15.4.1"
  },
  "devDependencies": {
    "@babel/core": "^7.9.6",
    "@babel/node": "^7.8.7",
    "@babel/plugin-proposal-class-properties": "^7.10.1",
    "@babel/plugin-transform-runtime": "^7.9.6",
    "@babel/preset-env": "^7.9.6",
    "@babel/preset-react": "^7.9.4",
    "@typescript-eslint/eslint-plugin": "^5.62.0",
    "@typescript-eslint/parser": "^5.62.0",
    "babel-loader": "^8.1.0",
    "css-loader": "^6.8.0",
    "eslint": "^8.56.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-plugin-import": "^2.29.0",
    "eslint-plugin-jsx-a11y": "^6.8.0",
    "eslint-plugin-react": "^7.33.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-simple-import-sort": "^10.0.0",
    "html-webpack-plugin": "^5.5.0",
    "husky": "^9.1.0",
    "jest": "^29.7.0",
    "mini-css-extract-plugin": "^2.7.0",
    "nodemon": "^3.1.0",
    "style-loader": "^3.3.0",
    "tmp": "^0.2.3",
    "ts-loader": "^9.4.0",
    "typescript": "^5.3.0",
    "walk-sync": "^2.2.0",
    "webpack": "^5.88.0",
    "webpack-cli": "^5.1.0",
    "webpack-node-externals": "^3.0.0"
  }
}


================================================
FILE: src/README.md
================================================
### Code Organization

* While `server/` and `client/` are self explanatory, `common/` and `plugins/` contain code that is used by both. On the other hand, `demo/` contains an isolated program for E2E testing and generating demo videos (which uses a separate `config.json` to avoid conflicts with the production database).
* Data Model: [`server/models.js`](server/models.js) contains the database schema, which is an excellent starting point. [`common/data_types/api.js`](common/data_types/api.js) is an interface that (almost) all datatypes need to implement, and the other files in that directory contain implementations of that API, along with additional utilities.
* Server: [`server/database.js`](server/database.js) is a wrapper over Sequelize, providing an useful API for "actions". [`server/actions.js`](server/actions.js) creates a registry for all the RPCs that the client can invoke, by looking at all files in [`server/actions/`](server/actions/). And finally, [`server/index.js`](server/index.js) initializes the webserver, and allows clients to invoke these actions.
* Client: [`client/index.js`](client/index.js) initializes React, which powers the whole UI. While [`common/SocketRPC.js`](common/SocketRPC.js) sets up a communication system between server and client, [`client/Common/Coordinator.js`](client/Common/Coordinator.js) allows communication between different UI components. [`client/Common/DataLoader.js`](client/Common/DataLoader.js) is a commonly used utility to not just load data once, but subscribe to changes and react to them (look for `this.broadcast` method calls in server-side actions), allowing different UI components to remain in sync.
* Plugins: This is custom logic that can be activated in the tool, augmenting core functionality, but is likely not relevant for everyone. See the README file in that directory for more details.

### Backup File Size Estimation

* (1 kilobyte / event) * (50 events / day) * (365 days / year) * (10 years) = 182,500,000 bytes < 200 MB for 10 years. Note that this estimation does not include other data types, but those are infrequently created, and not separately counted.
* The total size can reduced significantly by compressing the backup file if needed. JSON was picked for human readability, not for space efficiency. A simple experiment with "gzip" results in a file size that was 10% of the original.


================================================
FILE: src/client/Application/Application.js
================================================
import React from 'react';
import Col from 'react-bootstrap/Col';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';

import { Enum } from '../../common/data_types';
import DateUtils from '../../common/DateUtils';
import {
    Coordinator, DataLoader, DateContext, EnumSelectorSection, ModalStack,
    PluginDisplayComponent, PluginDisplayLocation, ScrollableSection, SettingsContext,
} from '../Common';
import { LogEventList } from '../LogEvent';
import { LogStructureList } from '../LogStructure';
import { LogTopicList } from '../LogTopic';
import PropTypes from '../prop-types';
import { ReminderSidebar } from '../Reminders';
import { SettingsSection } from '../Settings';
import BackupSection from './BackupSection';
import CreditsSection from './CreditsSection';
import DetailsSection from './DetailsSection';
import FavoritesSection from './FavoritesSection';
import IndexSection from './IndexSection';
import TabSection from './TabSection';
import URLState from './URLState';

const Layout = Enum([
    {
        label: 'Split',
        value: 'split',
    },
    {
        label: 'Left',
        value: 'left',
    },
    {
        label: 'Right',
        value: 'right',
    },
]);

const Widgets = Enum([
    {
        label: 'Show',
        value: 'show',
    },
    {
        label: 'Hide',
        value: 'hide',
    },
]);

class Applicaton extends React.Component {
    constructor(props) {
        super(props);
        this.state = { urlParams: null, settings: null, disabled: false };
        this.tabRef = React.createRef();
    }

    componentDidMount() {
        this.deregisterCallbacks = [
            URLState.init(),
            Coordinator.subscribe('url-change', (urlParams) => this.setState({ urlParams })),
        ];
        const urlParams = Coordinator.invoke('url-params');
        urlParams.tab = urlParams.tab || TabSection.Enum.LOG_EVENT;
        urlParams.layout = urlParams.layout || Layout.SPLIT;
        urlParams.widgets = urlParams.widgets || Widgets.SHOW;
        this.setState({ urlParams });

        this.dataLoader = new DataLoader({
            getInput: () => ({ name: 'settings-get' }),
            onData: (settings) => this.setState({ settings }),
        });
    }

    componentDidUpdate() {
        this.dataLoader.reload();
    }

    componentWillUnmount() {
        this.dataLoader.stop();
        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());
    }

    renderLeftSidebar() {
        return (
            <Col md={2} className="my-3">
                <ScrollableSection>
                    <TabSection
                        plugins={this.props.plugins}
                        value={this.state.urlParams.tab}
                        onChange={(tab) => Coordinator.invoke('url-update', { tab })}
                        ref={this.tabRef}
                    />
                    {this.state.urlParams.widgets === Widgets.SHOW
                        ? (
                            <ReminderSidebar disabled={this.state.disabled} />
                        )
                        : null}
                </ScrollableSection>
            </Col>
        );
    }

    renderCenterSection() {
        const { settings } = this.state;
        const { layout } = this.state.urlParams;
        let indexSection = null;
        if (this.tabRef.current) {
            const Component = this.tabRef.current.getComponent(this.state.urlParams.tab);
            indexSection = (
                <IndexSection
                    Component={Component}
                    dateRange={this.state.urlParams.dateRange}
                    search={this.state.urlParams.search}
                    disabled={this.state.disabled}
                    onChange={(params) => Coordinator.invoke('url-update', params)}
                />
            );
        } else {
            setTimeout(() => this.forceUpdate(), 0);
        }
        let detailsSection = (
            <DetailsSection
                item={this.state.urlParams.details}
                disabled={this.state.disabled}
                onChange={(details) => Coordinator.invoke('url-update', { details })}
            />
        );
        if (settings.display_two_details_sections) {
            detailsSection = (
                <ScrollableSection>
                    <DetailsSection
                        item={this.state.urlParams.details}
                        disabled={this.state.disabled}
                        onChange={(details) => Coordinator.invoke('url-update', { details })}
                    />
                    <div className="py-4" />
                    <DetailsSection
                        item={this.state.urlParams.details2}
                        disabled={this.state.disabled}
                        onChange={(details2) => Coordinator.invoke('url-update', { details2 })}
                    />
                </ScrollableSection>
            );
        }
        if (layout === Layout.SPLIT) {
            return (
                <>
                    <Col md={4} className="my-3">{indexSection}</Col>
                    <Col md={4} className="my-3">{detailsSection}</Col>
                </>
            );
        } if (layout === Layout.LEFT) {
            return (
                <Col md={8} className="my-3">{indexSection}</Col>
            );
        } if (layout === Layout.RIGHT) {
            return (
                <Col md={8} className="my-3">{detailsSection}</Col>
            );
        }
        return null;
    }

    renderRightSidebar() {
        const { settings } = this.state;
        const results = [];
        results.push(
            <PluginDisplayComponent
                key={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_TOP}
                plugins={this.props.plugins}
                location={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_TOP}
            />,
        );
        results.push(
            <EnumSelectorSection
                key="layout"
                label="Layout: "
                options={Layout.Options}
                value={this.state.urlParams.layout}
                onChange={(layout) => Coordinator.invoke('url-update', { layout })}
            />,
            <EnumSelectorSection
                key="widgets"
                label="Widgets: "
                options={Widgets.Options}
                value={this.state.urlParams.widgets}
                onChange={(widgets) => Coordinator.invoke('url-update', { widgets })}
            />,
            <BackupSection key="backup" />,
        );
        if (settings) {
            results.push(
                <SettingsSection
                    key="settings"
                    settings={settings}
                    plugins={this.props.plugins}
                />,
            );
        }
        results.push(
            <PluginDisplayComponent
                key={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_BOTTOM}
                plugins={this.props.plugins}
                location={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_BOTTOM}
            />,
        );
        if (this.state.urlParams.widgets === Widgets.SHOW) {
            results.push(...this.renderRightSidebarWidgets());
        }
        results.push(<CreditsSection key="credit" />);
        return (
            <Col md={2} className="my-3">
                <ScrollableSection>
                    {results}
                </ScrollableSection>
            </Col>
        );
    }

    renderRightSidebarWidgets() {
        const nameSortComparator = (left, right) => left.name.localeCompare(right.name);
        const results = [];
        results.push(
            <PluginDisplayComponent
                key={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_TOP}
                plugins={this.props.plugins}
                location={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_TOP}
            />,
        );
        results.push(
            <div key="favorites">
                <FavoritesSection
                    title="Favorite Events"
                    dataType="log-event"
                    ViewerComponent={LogEventList.Single}
                    viewerComponentProps={{ viewerComponentProps: { displayDate: true } }}
                    valueKey="logEvent"
                />
                <FavoritesSection
                    title="Favorite Topics"
                    dataType="log-topic"
                    sortComparator={nameSortComparator}
                    ViewerComponent={LogTopicList.Single}
                    valueKey="logTopic"
                />
                <FavoritesSection
                    title="Favorite Structures"
                    dataType="log-structure"
                    sortComparator={nameSortComparator}
                    ViewerComponent={LogStructureList.Single}
                    valueKey="logStructure"
                />
            </div>,
        );
        results.push(
            <PluginDisplayComponent
                key={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_BOTTOM}
                plugins={this.props.plugins}
                location={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_BOTTOM}
            />,
        );
        return results;
    }

    render() {
        if (!this.state.urlParams) {
            return null;
        } if (!this.state.settings) {
            return null;
        }
        const container = (
            <Container fluid>
                <Row>
                    {this.renderLeftSidebar()}
                    {this.renderCenterSection(this.state.urlParams.layout)}
                    {this.renderRightSidebar()}
                </Row>
                <ModalStack />
            </Container>
        );
        return (
            <SettingsContext.Provider value={this.state.settings}>
                <DateContext.Provider value={DateUtils.getContext(this.state.settings)}>
                    {container}
                </DateContext.Provider>
            </SettingsContext.Provider>
        );
    }
}

Applicaton.propTypes = {
    plugins: PropTypes.Custom.Plugins.isRequired,
};

export default Applicaton;


================================================
FILE: src/client/Application/BackupSection.js
================================================
import React from 'react';

import {
    Coordinator, DataLoader, LeftRight, SidebarSection,
} from '../Common';

class BackupSection extends React.Component {
    static onClick() {
        window.api.send('backup-save')
            .then(({ isUnchanged }) => Coordinator.invoke('modal-info', {
                title: 'Backup',
                message: isUnchanged ? 'Backup unchanged!' : 'Backup complete!',
            }));
    }

    constructor(props) {
        super(props);
        this.state = { latestBackup: null };
    }

    componentDidMount() {
        this.dataLoader = new DataLoader({
            getInput: () => ({
                name: 'backup-latest',
            }),
            onData: (latestBackup) => this.setState({ latestBackup }),
        });
    }

    componentWillUnmount() {
        this.dataLoader.stop();
    }

    render() {
        const { latestBackup } = this.state;
        return (
            <SidebarSection>
                <LeftRight>
                    <a
                        className="mr-2"
                        href="#"
                        onClick={() => BackupSection.onClick()}
                        title="Save New Backup"
                    >
                        Backup:
                    </a>
                    {latestBackup ? `${latestBackup.timetamp}` : 'No backup found!' }
                </LeftRight>
            </SidebarSection>
        );
    }
}

export default BackupSection;


================================================
FILE: src/client/Application/CreditsSection.js
================================================
import React from 'react';

import { SidebarSection } from '../Common';

function CreditsSection(props) {
    return (
        <SidebarSection>
            {'Built by: '}
            <a href="http://kaustubh.io">
                Kaustubh Karkare
            </a>
            {' | '}
            <a href="https://github.com/kaustubh-karkare/glados">
                GitHub
            </a>
        </SidebarSection>
    );
}

export default CreditsSection;


================================================
FILE: src/client/Application/DetailsSection.css
================================================
.details-section .scrollable-section .text-editor {
    background-color: var(--component-color);
    padding: 4px;
}

.details-section .scrollable-section .public-DraftEditor-content {
    min-height: 200px;
}


================================================
FILE: src/client/Application/DetailsSection.js
================================================
import './DetailsSection.css';

import React from 'react';
import Button from 'react-bootstrap/Button';
import InputGroup from 'react-bootstrap/InputGroup';
import {
    MdCheckCircle, MdClose, MdEdit, MdFavorite, MdFavoriteBorder, MdSearch,
} from 'react-icons/md';
import { RiLoaderLine } from 'react-icons/ri';

import RichTextUtils from '../../common/RichTextUtils';
import {
    Coordinator, DataLoader, debounce,
    ScrollableSection, SettingsContext, TextEditor, TypeaheadOptions, TypeaheadSelector,
} from '../Common';
import { LogEventDetailsHeader, LogEventEditor } from '../LogEvent';
import { LogValueListEditor } from '../LogKey';
import { LogStructureDetailsHeader, LogStructureEditor } from '../LogStructure';
import { LogTopicDetailsHeader, LogTopicEditor, LogTopicOptions } from '../LogTopic';
import PropTypes from '../prop-types';

const HEADER_MAPPING = {
    'log-event': {
        HeaderComponent: LogEventDetailsHeader,
        EditorComponent: LogEventEditor,
        valueKey: 'logEvent',
    },
    'log-structure': {
        HeaderComponent: LogStructureDetailsHeader,
        EditorComponent: LogStructureEditor,
        valueKey: 'logStructure',
    },
    'log-topic': {
        HeaderComponent: LogTopicDetailsHeader,
        EditorComponent: LogTopicEditor,
        valueKey: 'logTopic',
    },
};

class DetailsSection extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            item: null,
            isDirty: false,
            isSaveDisabled: false,
        };
        this.saveDebounced = debounce(this.saveNotDebounced, 500);
    }

    componentDidMount() {
        this.dataLoader = new DataLoader({
            getInput: () => {
                const { item } = this.props;
                if (!item) {
                    return null;
                } if (item.__type__ in HEADER_MAPPING) {
                    return {
                        name: `${item.__type__}-load`,
                        args: { __id__: item.__id__ },
                    };
                }
                return null;
            },
            onData: (newItem) => {
                const oldItem = this.state.item;
                if (
                    oldItem
                    && newItem
                    && oldItem.__type__ === newItem.__type__
                    && oldItem.__id__ === newItem.__id__
                ) {
                    this.setState((state) => {
                        const { details } = state.item; // copy local details
                        state.item = { ...newItem, details };
                        return state;
                    });
                } else {
                    this.setState({ item: newItem });
                }
            },
            onError: () => {
                const { item } = this.props;
                if (item) {
                    Coordinator.invoke(
                        'modal-error',
                        `${JSON.stringify(item, null, 4)}\n\nThis item does support details!`,
                    );
                }
                this.props.onChange(null);
            },
        });
    }

    componentDidUpdate() {
        this.dataLoader.reload();
    }

    componentWillUnmount() {
        this.dataLoader.stop();
    }

    onChange(item) {
        this.setState((state) => {
            state.item = item;
            state.isDirty = true;
            return state;
        }, this.saveDebounced);
    }

    onEditButtonClick() {
        const { item } = this.state;
        const { EditorComponent, valueKey } = HEADER_MAPPING[item.__type__];
        Coordinator.invoke('modal-editor', {
            dataType: item.__type__,
            EditorComponent,
            valueKey,
            value: item,
        });
    }

    saveNotDebounced() {
        if (this.state.isSaveDisabled) {
            return;
        }
        const { item } = this.state;
        if (item) {
            window.api.send(`${item.__type__}-upsert`, item)
                .then((newItem) => this.setState({
                    isDirty: !RichTextUtils.equals(item.details, newItem.details),
                }));
        }
    }

    renderPrefixButtons(item) {
        const buttons = [];
        const { HeaderComponent } = HEADER_MAPPING[item.__type__];
        if (HeaderComponent.onSearchButtonClick) {
            buttons.push(
                <Button
                    key="search"
                    onClick={() => HeaderComponent.onSearchButtonClick(item)}
                    title="Search"
                >
                    <MdSearch />
                </Button>,
            );
        }
        if (typeof item.isFavorite === 'boolean') {
            buttons.push(
                <Button
                    key="favorite"
                    onClick={() => this.onChange({ ...item, isFavorite: !item.isFavorite })}
                    title="Favorite"
                >
                    {item.isFavorite ? <MdFavorite /> : <MdFavoriteBorder />}
                </Button>,
            );
        }
        return buttons;
    }

    renderSuffixButtons(item) {
        return [
            <Button key="edit" title="Edit" onClick={() => this.onEditButtonClick()}>
                <MdEdit />
            </Button>,
            <Button key="status" title="Status">
                {this.state.isDirty ? <RiLoaderLine /> : <MdCheckCircle />}
            </Button>,
            <Button
                key="close"
                title="Close"
                onClick={() => this.props.onChange(null)}
            >
                <MdClose />
            </Button>,
        ];
    }

    renderHeader() {
        const { item } = this.state;
        if (item && item.__type__ in HEADER_MAPPING) {
            const { HeaderComponent, valueKey } = HEADER_MAPPING[item.__type__];
            const headerComponentProps = { [valueKey]: item };
            return (
                <InputGroup>
                    {this.renderPrefixButtons(item)}
                    <HeaderComponent {...headerComponentProps} />
                    {this.renderSuffixButtons(item)}
                </InputGroup>
            );
        }

        const options = new TypeaheadOptions({
            serverSideOptions: [
                { name: 'log-topic' },
                { name: 'log-structure' },
            ],
        });
        return (
            <InputGroup>
                <TypeaheadSelector
                    id="details-section-topic-or-structure"
                    options={options}
                    value={null}
                    disabled={this.props.disabled}
                    onChange={(newItem) => this.props.onChange(newItem)}
                    placeholder="Details ..."
                />
            </InputGroup>
        );
    }

    renderKeys() {
        const { item } = this.state;
        let logKeys = null;
        if (!item) {
            // nothing
        } else if (item.__type__ === 'log-event') {
            logKeys = item.logStructure && item.logStructure.logKeys;
        } else if (item.__type__ === 'log-topic') {
            logKeys = item.parentLogTopic && item.parentLogTopic.childKeys;
        }
        if (!logKeys) {
            return null;
        }
        return (
            <LogValueListEditor
                source={item}
                logKeys={logKeys}
                disabled
                onChange={() => null}
            />
        );
    }

    renderDetails() {
        const { item } = this.state;
        if (!item) {
            return null;
        }
        if (
            item.__type__ === 'log-event'
            && item.logStructure
            && !item.logStructure.eventAllowDetails
        ) {
            return <div>(disabled by structure)</div>;
        }
        const parentLogTopic = item && item.__type__ === 'log-topic' ? item : undefined;
        return (
            <div>
                <TextEditor
                    unstyled
                    value={item.details}
                    onChange={(details) => this.onChange({ ...item, details })}
                    options={LogTopicOptions.get({
                        allowCreation: true,
                        parentLogTopic,
                        beforeSelect: () => this.setState({ isSaveDisabled: true }),
                        afterSelect: () => this.setState({ isSaveDisabled: false }),
                    })}
                />
            </div>
        );
    }

    render() {
        const settings = this.context;
        if (settings.display_two_details_sections) {
            return (
                <div className="details-section">
                    <div className="mb-1">
                        {this.renderHeader()}
                    </div>
                    {this.renderKeys()}
                    {this.renderDetails()}
                </div>
            );
        }
        return (
            <div className="details-section">
                <div className="mb-1">
                    {this.renderHeader()}
                </div>
                <ScrollableSection padding={20 + 4}>
                    {this.renderDetails()}
                </ScrollableSection>
            </div>
        );
    }
}

DetailsSection.propTypes = {
    item: PropTypes.Custom.Item,
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
};

DetailsSection.contextType = SettingsContext;

export default DetailsSection;


================================================
FILE: src/client/Application/FavoritesSection.js
================================================
import PropTypes from 'prop-types';
import React from 'react';

import { DataLoader, SidebarSection } from '../Common';

class FavoritesSection extends React.Component {
    constructor(props) {
        super(props);
        this.state = { items: null };
    }

    componentDidMount() {
        this.dataLoader = new DataLoader({
            getInput: () => ({
                name: `${this.props.dataType}-list`,
                args: {
                    where: { isFavorite: true },
                },
            }),
            onData: (items) => {
                if (this.props.sortComparator) {
                    items = items.sort(this.props.sortComparator);
                }
                this.setState({ items });
            },
        });
    }

    componentWillUnmount() {
        this.dataLoader.stop();
    }

    renderContent() {
        if (this.state.items === null) {
            return 'Loading ...';
        }
        const { ViewerComponent, viewerComponentProps, valueKey } = this.props;
        return this.state.items.map((item) => (
            <ViewerComponent
                key={item.__id__}
                {...viewerComponentProps}
                {...{ [valueKey]: item }}
            />
        ));
    }

    render() {
        return (
            <SidebarSection title={this.props.title}>
                {this.renderContent()}
            </SidebarSection>
        );
    }
}

FavoritesSection.propTypes = {
    title: PropTypes.string.isRequired,
    dataType: PropTypes.string.isRequired,
    sortComparator: PropTypes.func,
    ViewerComponent: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    viewerComponentProps: PropTypes.object,
    valueKey: PropTypes.string.isRequired,
};

FavoritesSection.defaultProps = {
    viewerComponentProps: {},
};

export default FavoritesSection;


================================================
FILE: src/client/Application/IndexSection.css
================================================
.index-section {
    margin-bottom: 128px;
}

.index-section .text-editor {
    max-width: 500px;
}


================================================
FILE: src/client/Application/IndexSection.js
================================================
import React from 'react';
import InputGroup from 'react-bootstrap/InputGroup';

import { DateRangePicker, ScrollableSection, TypeaheadSelector } from '../Common';
import PropTypes from '../prop-types';

class IndexSection extends React.Component {
    renderWithTypeahead() {
        const { Component, dateRange, onChange } = this.props;
        const typeaheadOptions = Component.getTypeaheadOptions();
        const filteredSearch = typeaheadOptions.filterToKnownTypes(this.props.search);
        if (filteredSearch.length !== this.props.search.length) {
            window.setTimeout(onChange.bind(filteredSearch), 0);
        }
        return (
            <div className="index-section">
                <div className="mb-1">
                    <InputGroup>
                        <DateRangePicker
                            dateRange={dateRange}
                            onChange={(newDateRange) => onChange({ dateRange: newDateRange })}
                        />
                        <TypeaheadSelector
                            id="search"
                            options={typeaheadOptions}
                            value={filteredSearch}
                            disabled={this.props.disabled}
                            onChange={(search) => onChange({ search })}
                            placeholder="Search ..."
                            multiple
                        />
                    </InputGroup>
                </div>
                <ScrollableSection padding={20 + 4}>
                    <Component
                        dateRange={dateRange}
                        search={filteredSearch}
                    />
                </ScrollableSection>
            </div>
        );
    }

    renderSimple() {
        const { Component } = this.props;
        return (
            <div className="index-section">
                <ScrollableSection>
                    <Component />
                </ScrollableSection>
            </div>
        );
    }

    render() {
        const { Component } = this.props;
        if (Component.getTypeaheadOptions) {
            return this.renderWithTypeahead();
        }
        return this.renderSimple();
    }
}

IndexSection.propTypes = {
    Component: PropTypes.func.isRequired,
    dateRange: PropTypes.Custom.DateRange,
    search: PropTypes.arrayOf(PropTypes.Custom.Item.isRequired).isRequired,
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
};

export default IndexSection;


================================================
FILE: src/client/Application/TabSection.js
================================================
import React from 'react';

import { Enum } from '../../common/data_types';
import { PluginDisplayLocation, SettingsContext, SidebarSection } from '../Common';
import { GraphSection } from '../Graphs';
import { LogEventSearch } from '../LogEvent';
import { LogStructureSearch } from '../LogStructure';
import { LogTopicSearch } from '../LogTopic';
import PropTypes from '../prop-types';

const Tab = Enum([
    {
        label: 'Manage Events',
        value: 'log-event',
        Component: LogEventSearch,
    },
    {
        label: 'Manage Topics',
        value: 'log-topic',
        Component: LogTopicSearch,
    },
    {
        label: 'Manage Structures',
        value: 'log-structure',
        Component: LogStructureSearch,
    },
    {
        label: 'Explore Graphs',
        value: 'graph',
        Component: GraphSection,
    },
]);

class TabSection extends React.Component {
    constructor(props) {
        super(props);
        const PluginOptions = [];
        const TabComponents = {};
        Tab.Options.forEach((option) => {
            TabComponents[option.value] = option.Component;
        });
        Object.entries(this.props.plugins).forEach(([_name, api]) => {
            if (api.getDisplayLocation() === PluginDisplayLocation.TAB_SECTION) {
                const tabData = api.getTabData();
                PluginOptions.push(tabData);
                TabComponents[tabData.value] = () => (
                    <SettingsContext.Consumer>
                        {(settings) => {
                            const key = api.getSettingsKey();
                            return api.getDisplayComponent({
                                settings: key ? settings[key] : null,
                            });
                        }}
                    </SettingsContext.Consumer>
                );
            }
        });
        this.state = {
            options: Tab.Options.concat(PluginOptions),
            components: TabComponents,
        };
    }

    getComponent(value) {
        return this.state.components[value];
    }

    render() {
        return this.state.options.map((option) => (
            <SidebarSection
                key={option.value}
                onClick={() => this.props.onChange(option.value)}
                selected={this.props.value === option.value}
            >
                {option.label}
            </SidebarSection>
        ));
    }
}

TabSection.Enum = Tab;

TabSection.propTypes = {
    plugins: PropTypes.Custom.Plugins.isRequired,
    value: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
};

export default TabSection;


================================================
FILE: src/client/Application/URLState.js
================================================
import { Coordinator, DateRangePicker, URLManager } from '../Common';

/**
 * [...Array(128).keys()]
 *     .map(code => String.fromCharCode(code))
 *     .filter(char => !char.match(/\w/) && char === encodeURIComponent(char))
 * ["!", "'", "(", ")", "*", "-", ".", "~"]
 * Picked the one most easily readable in the URL.
 */
const SEPARATOR = '~';

function serializeItem(item) {
    return `${item.__type__}${SEPARATOR}${item.__id__}${SEPARATOR}${item.name}`;
}

function deserializeItem(token) {
    const [__type__, __id__, name] = token.split(SEPARATOR);
    return { __type__, __id__: parseInt(__id__, 10), name };
}

class URLState {
    static getStateFromURL() {
        const params = URLManager.get();
        return {
            tab: params.tab,
            layout: params.layout,
            widgets: params.widgets,
            dateRange: DateRangePicker.deserialize(params.date_range),
            search: params.search ? params.search.map(deserializeItem) : [],
            details: params.details ? deserializeItem(params.details) : null,
            // settings.display_two_details_sections
            details2: params.details2 ? deserializeItem(params.details2) : null,
        };
    }

    static getURLFromState(state) {
        const params = {
            tab: state.tab,
            layout: state.layout,
            widgets: state.widgets,
            date_range: DateRangePicker.serialize(state.dateRange),
            search: state.search ? state.search.map(serializeItem) : undefined,
            details: state.details ? serializeItem(state.details) : undefined,
            // settings.display_two_details_sections
            details2: state.details2 ? serializeItem(state.details2) : undefined,
        };
        return URLManager.getLink(params);
    }

    static init() {
        const instance = new URLState();
        return () => instance.cleanup();
    }

    constructor() {
        this.deregisterCallbacks = [
            URLManager.init(() => this.onChange()),
            Coordinator.register('url-params', () => this.state),
            Coordinator.register('url-link', (data) => this.getLink(data)),
            Coordinator.register('url-update', (data) => this.onUpdate(data)),
        ];
        this.onChange(); // set this.state
    }

    cleanup() {
        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());
    }

    onChange() {
        this.state = URLState.getStateFromURL();
        Coordinator.broadcast('url-change', this.state);
    }

    getLink(methodOrData) {
        let newState;
        if (typeof methodOrData === 'function') {
            newState = methodOrData(this.state) || this.state;
        } else {
            newState = { ...this.state, ...methodOrData };
        }
        return URLState.getURLFromState(newState);
    }

    onUpdate(methodOrData) {
        URLManager.update(this.getLink(methodOrData));
    }
}

export default URLState;


================================================
FILE: src/client/Application/index.js
================================================
// eslint-disable-next-line import/prefer-default-export
export { default as Application } from './Application';


================================================
FILE: src/client/Bootstrap/InputGroup.css
================================================
.input-group:focus {
    outline: none;
}

.input-group > * {
    border-style: solid;
    border-color: transparent;
    border-radius: 0;
    border-width: 0px 0px;
    font-size: var(--font-size);
    height: 20px;
}

.input-group *:focus {
    outline: none;
}

.input-group > :first-child {
    border-top-left-radius: 2px;
    border-bottom-left-radius: 2px;
    border-left-width: 0;
}

.input-group > :last-child {
    border-top-right-radius: 2px;
    border-bottom-right-radius: 2px;
    border-right-width: 0;
}

.input-group > .input-group-text {
    background: var(--component-color);
    display: block;
    padding: 1px;
    text-align: center;
    width: 128px;
}

.input-group > .btn {
    background: var(--input-background-color);
    width: 20px;
    padding: 0px;
}

.input-group > .btn > svg {
    position: relative;
    top: -1px;
}

.input-group > input.form-control {
    background-color: var(--input-background-color);
    color: var(--input-text-color);
    height: 20px;
    padding: 0 4px;
}

.input-group > select.form-control {
    background-color: var(--input-background-color);
    color: var(--input-text-color);
    padding: 0;
}

.input-group > .form-check-inline {
    margin-right: 0;
    width: 16px;
}

.input-group > .rbt input:first-child {
    border: none;
    border-radius: 0;
    border-width: 0 1px;
    padding: 0 4px;
    height: 20px;
}

.input-group > .rbt input:first-child[disabled] {
    background-color: var(--input-disabled-background-color);;
}

.input-group > .text-editor {
    flex-grow: 1;
    width: 1px;
    height: auto;
}


================================================
FILE: src/client/Bootstrap/Modal.css
================================================
.modal-dialog {
    max-width: 800px;
}

.modal-content {
    background-color: var(--background-color);
    border-color: var(--component-highlight-color);
    color: var(--text-color);
}


================================================
FILE: src/client/Bootstrap/Popover.css
================================================
.popover {
    max-width: none;
}

.popover-header,
.popover-body {
    background-color: var(--input-background-color);
}


================================================
FILE: src/client/Bootstrap/index.js
================================================
import 'bootstrap/dist/css/bootstrap.min.css';
import './InputGroup.css';
import './Modal.css';
import './Popover.css';


================================================
FILE: src/client/Common/AddLinkPlugin.js
================================================
// https://bitwiser.in/2017/05/11/creating-rte-part-3-entities-and-decorators.html

/* eslint-disable */

import React from 'react';
import {
    RichUtils,
    KeyBindingUtil,
    EditorState,
} from 'draft-js';


export const linkStrategy = (contentBlock, callback, contentState) => {
    contentBlock.findEntityRanges(
        (character) => {
            const entityKey = character.getEntity();
            return (
                entityKey !== null
        && contentState.getEntity(entityKey).getType() === 'LINK'
            );
        },
        callback,
    );
};


export const Link = (props) => {
    const { contentState, entityKey } = props;
    const { url } = contentState.getEntity(entityKey).getData();
    return (
        <a
            className="link"
            href={url}
            rel="noopener noreferrer"
            target="_blank"
            aria-label={url}
        >
            {props.children}
        </a>
    );
};

const AddLinkPlugin = {
    keyBindingFn(event, { getEditorState }) {
        const editorState = getEditorState();
        const selection = editorState.getSelection();
        // Don't do anything if no text is selected.
        if (selection.isCollapsed()) {
            return;
        }
        if (KeyBindingUtil.hasCommandModifier(event) && event.which === 75) {
            return 'add-link';
        }
    },

    handleKeyCommand(command, editorState, eventTimeStamp, { getEditorState, setEditorState }) {
        if (command !== 'add-link') {
            return 'not-handled';
        }
        const link = window.prompt('Paste the link:');
        const selection = editorState.getSelection();
        if (!link) {
            setEditorState(RichUtils.toggleLink(editorState, selection, null));
            return 'handled';
        }
        const content = editorState.getCurrentContent();
        const contentWithEntity = content.createEntity('LINK', 'MUTABLE', { url: link });
        const newEditorState = EditorState.push(editorState, contentWithEntity, 'create-entity');
        const entityKey = contentWithEntity.getLastCreatedEntityKey();
        setEditorState(RichUtils.toggleLink(newEditorState, selection, entityKey));
        return 'handled';
    },

    decorators: [{
        strategy: linkStrategy,
        component: Link,
    }],
};

export default AddLinkPlugin;


================================================
FILE: src/client/Common/AsyncSelector.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import Form from 'react-bootstrap/Form';

import DataLoader from './DataLoader';

class AsyncSelector extends React.Component {
    constructor(props) {
        super(props);
        this.state = { options: null };
    }

    componentDidMount() {
        this.dataLoader = new DataLoader({
            getInput: () => this.props.options,
            onData: (options) => this.setState({
                options: [...this.props.prefixOptions, ...options, ...this.props.suffixOptions],
            }),
        });
    }

    componentDidUpdate(prevProps) {
        this.dataLoader.reload();
    }

    componentWillUnmount() {
        this.dataLoader.stop();
    }

    onChange(id) {
        if (this.state.options) {
            const selectedOption = this.state.options.find(
                (option) => option.id.toString() === id,
            );
            if (selectedOption) {
                this.props.onChange(selectedOption);
            }
        }
    }

    render() {
        const options = this.state.options || [this.props.value];
        return (
            <Form.Control
                as="select"
                value={this.props.value.__id__}
                disabled={this.props.disabled}
                onChange={(event) => this.onChange(event.target.value)}
            >
                {options.map((item) => {
                    const optionProps = { key: item.__id__, value: item.__id__ };
                    return <option {...optionProps}>{item[this.props.labelKey]}</option>;
                })}
            </Form.Control>
        );
    }
}

AsyncSelector.propTypes = {
    labelKey: PropTypes.string,
    // eslint-disable-next-line react/forbid-prop-types
    value: PropTypes.object,
    // eslint-disable-next-line react/forbid-prop-types
    prefixOptions: PropTypes.array,
    // eslint-disable-next-line react/forbid-prop-types
    options: PropTypes.object,
    // eslint-disable-next-line react/forbid-prop-types
    suffixOptions: PropTypes.array,
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
};

AsyncSelector.defaultProps = {
    labelKey: 'name',
    prefixOptions: [],
    suffixOptions: [],
};

export default AsyncSelector;


================================================
FILE: src/client/Common/BulletList/BulletList.css
================================================
.bullet-list .pager {
    color: var(--text-disabled-color);
}

.bullet-list .pager > span {
    cursor: pointer;
}

.bullet-list .pager > span:hover {
    color: var(--text-color);
}


================================================
FILE: src/client/Common/BulletList/BulletList.js
================================================
import './BulletList.css';

import arrayMove from 'array-move';
import classNames from 'classnames';
import deepEqual from 'deep-equal';
import PropTypes from 'prop-types';
import React from 'react';
import { SortableContainer, SortableElement } from 'react-sortable-hoc';

import { getDataTypeMapping } from '../../../common/data_types';
import DataLoader from '../DataLoader';
import SettingsContext from '../SettingsContext';
import BulletListItem from './BulletListItem';
import BulletListLine from './BulletListLine';
import BulletListPager from './BulletListPager';
import BulletListTitle from './BulletListTitle';

const WrappedContainer = SortableContainer(({ children }) => <div>{children}</div>);
const SortableBulletListItem = SortableElement(BulletListItem);

class BulletList extends React.Component {
    static getDerivedStateFromProps(props, state) {
        if (state.items) {
            state.areAllExpanded = state.items
                .every((item) => state.isExpanded[item.__id__]);
        }
        return state;
    }

    constructor(props) {
        super(props);
        const pageSize = parseInt(props.settings.bullet_list_page_size, 10) || 25;
        this.state = {
            items: null,
            isExpanded: {},
            areAllExpanded: true,
            pageSize,
            limit: pageSize,
        };
    }

    componentDidMount() {
        this.dataLoader = new DataLoader({
            getInput: () => ({
                name: `${this.props.dataType}-list`,
                args: {
                    where: this.props.where,
                    limit: this.state.limit !== null ? this.state.limit + 1 : undefined,
                },
            }),
            onData: (items) => {
                if (this.state.limit && items.length > this.state.limit) {
                    this.setState({ items: items.slice(1), hasMoreItems: true });
                } else {
                    this.setState({ items, hasMoreItems: false, limit: null });
                }
            },
        });
    }

    componentDidUpdate(prevProps) {
        if (
            prevProps.dataType !== this.props.dataType
            || !deepEqual(prevProps.where, this.props.where)
        ) {
            this.updateLimit(this.state.pageSize);
        }
    }

    componentWillUnmount() {
        this.dataLoader.stop();
    }

    onAddButtonClick(event) {
        const DataType = getDataTypeMapping()[this.props.dataType];
        const value = DataType.createVirtual(this.props.where);
        const context = { ...this };
        context.props = { ...context.props, value };
        BulletListItem.prototype.onEdit.call(context, event);
    }

    onSortButtonClick(event) {
        const input = {
            dataType: this.props.dataType,
            where: this.props.where,
        };
        window.api.send(`${this.props.dataType}-sort`, input);
    }

    onMove(index, delta, event) {
        if (!event.shiftKey) return;
        const otherIndex = index + delta;
        const totalLength = this.state.items.length;
        if (otherIndex < 0 || otherIndex === totalLength) return;
        this.onReorder({ oldIndex: index, newIndex: otherIndex });
    }

    onReorder({ oldIndex, newIndex }) {
        if (!this.props.allowReordering) return;
        const orderedItems = arrayMove(this.state.items, oldIndex, newIndex);
        const input = {
            dataType: this.props.dataType,
            where: this.props.where,
            ordering: orderedItems.map((item) => item.__id__),
        };
        window.api.send(`${this.props.dataType}-reorder`, input)
            .then(() => this.setState({ items: orderedItems }));
    }

    updateLimit(limit) {
        this.setState({ limit, items: null }, () => this.dataLoader.reload());
    }

    renderItems() {
        if (!this.state.items) {
            return (
                <BulletListLine>
                    <span>Loading ...</span>
                </BulletListLine>
            );
        }
        return this.state.items.map((item, index) => (
            <SortableBulletListItem
                index={index}
                key={item.__id__}
                dataType={this.props.dataType}
                valueKey={this.props.valueKey}
                ViewerComponent={this.props.ViewerComponent}
                viewerComponentProps={this.props.viewerComponentProps}
                EditorComponent={this.props.EditorComponent}
                allowReordering={this.props.allowReordering}
                prefixActions={this.props.prefixActions
                    .map((action) => ({ ...action, perform: action.perform.bind(null, item) }))}
                onMoveUp={(event) => this.onMove(index, -1, event)}
                onMoveDown={(event) => this.onMove(index, 1, event)}
                isExpanded={this.state.isExpanded[item.__id__] || false}
                setIsExpanded={(isExpanded) => this.setState((state) => {
                    state.isExpanded[item.__id__] = isExpanded;
                    return state;
                })}
                value={item}
                dragHandleSpace
            />
        ));
    }

    renderAdder() {
        const { AdderComponent } = this.props;
        if (!AdderComponent) {
            return null;
        }
        return (
            <BulletListLine>
                <AdderComponent where={this.props.where} />
            </BulletListLine>
        );
    }

    render() {
        return (
            <div className={classNames('bullet-list', this.props.className)}>
                <BulletListTitle
                    name={this.props.name}
                    areAllExpanded={this.state.areAllExpanded}
                    onToggleButtonClick={() => this.setState((state) => {
                        if (state.areAllExpanded) {
                            return { isExpanded: {} };
                        }
                        return {
                            isExpanded: Object.fromEntries(
                                state.items.map((item) => [item.__id__, true]),
                            ),
                        };
                    })}
                    onAddButtonClick={this.props.allowCreation
                        ? (event) => this.onAddButtonClick(event)
                        : null}
                    onSortButtonClick={this.props.allowSorting
                        ? (event) => this.onSortButtonClick(event)
                        : null}
                />
                <BulletListPager
                    batchSize={this.state.pageSize}
                    limit={this.state.limit}
                    updateLimit={(limit) => this.updateLimit(limit)}
                    itemsLength={this.state.items ? this.state.items.length : null}
                    hasMoreItems={this.state.hasMoreItems}
                />
                <WrappedContainer
                    helperClass="sortableDraggedItem"
                    useDragHandle
                    onSortEnd={(data) => this.onReorder(data)}
                >
                    {this.renderItems()}
                </WrappedContainer>
                {this.renderAdder()}
            </div>
        );
    }
}

BulletList.propTypes = {
    name: PropTypes.string.isRequired,
    dataType: PropTypes.string.isRequired,
    valueKey: PropTypes.string.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    where: PropTypes.object,
    allowCreation: PropTypes.bool,
    allowSorting: PropTypes.bool,
    allowReordering: PropTypes.bool,
    ViewerComponent: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    viewerComponentProps: PropTypes.object,
    EditorComponent: PropTypes.func.isRequired,
    AdderComponent: PropTypes.func,
    // eslint-disable-next-line react/forbid-prop-types
    prefixActions: PropTypes.array,
    className: PropTypes.string,
    // eslint-disable-next-line react/forbid-prop-types
    settings: PropTypes.object.isRequired,
};

BulletList.defaultProps = {
    allowCreation: true,
    prefixActions: [],
};

const WrappedBulletList = SettingsContext.Wrapper(BulletList);
WrappedBulletList.Item = BulletListItem;

export default WrappedBulletList;


================================================
FILE: src/client/Common/BulletList/BulletListIcon.js
================================================
import PropTypes from 'prop-types';
import React from 'react';

import { KeyCodes } from '../Utils';

function BulletListIcon(props) {
    return (
        <div
            className="icon ml-1"
            title={props.title}
            onClick={props.onClick}
            onKeyDown={(event) => {
                if (event.keyCode === KeyCodes.ENTER) {
                    props.onClick(event);
                }
            }}
        >
            {props.children}
        </div>
    );
}

BulletListIcon.propTypes = {
    onClick: PropTypes.func.isRequired,
    title: PropTypes.string.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any.isRequired,
};

export default BulletListIcon;


================================================
FILE: src/client/Common/BulletList/BulletListItem.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import InputGroup from 'react-bootstrap/InputGroup';
import { BsList } from 'react-icons/bs';
import { GoPrimitiveDot } from 'react-icons/go';
import { MdEdit, MdFormatLineSpacing } from 'react-icons/md';
import { TiMinus, TiPlus } from 'react-icons/ti';
import { SortableHandle } from 'react-sortable-hoc';

import Coordinator from '../Coordinator';
import Dropdown from '../Dropdown';
import Highlightable from '../Highlightable';
import Icon from '../Icon';
import InputLine from '../InputLine';
import { KeyCodes } from '../Utils';

const SortableDragHandle = SortableHandle(() => (
    <Icon className="sortableDragHandle" title="Reorder">
        <MdFormatLineSpacing />
    </Icon>
));

class BulletListItem extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isHighlighted: false, isExpanded: false };
        this.dropdownRef = React.createRef();
    }

    onEdit(event) {
        if (event) {
            // Don't let enter propagate to EditorModal.
            event.preventDefault();
            event.stopPropagation();
        }
        if (event && event.shiftKey) {
            Coordinator.invoke('url-update', { details: this.props.value });
            return;
        }
        Coordinator.invoke('modal-editor', {
            dataType: this.props.dataType,
            EditorComponent: this.props.EditorComponent,
            valueKey: this.props.valueKey,
            value: this.props.value,
        });
    }

    onDelete(event) {
        if (event) {
            // Don't let enter propagate to ConfirmationModal.
            event.preventDefault();
            event.stopPropagation();
        }
        if (event && !event.shiftKey) {
            Coordinator.invoke('modal-confirm', {
                title: 'Confirm deletion?',
                body: this.renderViewer(),
                onClose: (result) => {
                    if (result) this.onDelete();
                },
            });
            return;
        }
        window.api.send(`${this.props.dataType}-delete`, this.props.value.__id__);
    }

    onKeyDown(event) {
        if (event.keyCode === KeyCodes.SPACE) {
            this.setIsExpanded(!this.getIsExpanded());
        } else if (event.keyCode === KeyCodes.ENTER) {
            this.onEdit(event);
        } else if (event.keyCode === KeyCodes.DELETE) {
            this.onDelete(event);
        } else if (event.keyCode === KeyCodes.UP_ARROW) {
            if (this.props.allowReordering) this.props.onMoveUp(event);
        } else if (event.keyCode === KeyCodes.DOWN_ARROW) {
            if (this.props.allowReordering) this.props.onMoveDown(event);
        }
    }

    getIsExpanded() {
        if (typeof this.props.isExpanded !== 'undefined') {
            return this.props.isExpanded;
        }
        return this.state.isExpanded;
    }

    setIsExpanded(isExpanded) {
        if (typeof this.props.isExpanded !== 'undefined') {
            this.props.setIsExpanded(isExpanded);
        } else {
            this.setState({ isExpanded });
        }
    }

    setIsHighlighted(isHighlighted) {
        if (!isHighlighted && this.dropdownRef.current) {
            this.dropdownRef.current.hide();
        }
        this.setState({ isHighlighted });
    }

    getViewerProps() {
        return { [this.props.valueKey]: this.props.value, ...this.props.viewerComponentProps };
    }

    renderDragHandle() {
        if (!this.props.dragHandleSpace) return null;
        if (this.state.isHighlighted && this.props.allowReordering) return <SortableDragHandle />;
        return <Icon />;
    }

    renderBullet() {
        const isExpanded = this.getIsExpanded();
        const iconProps = {
            alwaysHighlighted: true,
            className: 'mr-1',
            title: isExpanded ? 'Collapse' : 'Expand',
        };
        if (this.state.isHighlighted) {
            return (
                <Icon {...iconProps} onClick={() => this.setIsExpanded(!isExpanded)}>
                    {isExpanded ? <TiMinus /> : <TiPlus />}
                </Icon>
            );
        }
        return (
            <Icon {...iconProps}>
                {isExpanded ? <TiMinus /> : <GoPrimitiveDot />}
            </Icon>
        );
    }

    renderEditButton() {
        if (!this.state.isHighlighted) {
            return null;
        }
        return <MdEdit onClick={(event) => this.onEdit(event)} />;
    }

    renderActionsDropdown() {
        if (!this.state.isHighlighted) {
            return null;
        }
        const actions = [...this.props.prefixActions];
        actions.push({
            __id__: 'delete',
            name: 'Delete',
            perform: (event) => this.onDelete(event),
        });
        actions.push({
            __id__: 'info',
            name: 'Debug Info',
            perform: (_event) => Coordinator.invoke(
                'modal-info',
                {
                    title: 'Debug Info',
                    message: <pre>{JSON.stringify(this.props.value, null, 4)}</pre>,
                },
            ),
        });
        return (
            <Dropdown
                disabled={false}
                options={actions}
                onChange={(action, event) => action.perform(event)}
                ref={this.dropdownRef}
            >
                <BsList
                    onMouseOver={() => {
                        if (this.dropdownRef.current) {
                            this.dropdownRef.current.show();
                        }
                    }}
                />
            </Dropdown>
        );
    }

    renderExpanded() {
        if (!this.getIsExpanded()) {
            return null;
        }
        // 13 = width of 1 icon. 4 = margin right of bullet icon
        const marginLeft = 13 * (this.props.dragHandleSpace ? 2 : 1) + 4;
        return (
            <div style={{ marginLeft }}>
                {this.renderExpandedViewer()}
            </div>
        );
    }

    renderViewer() {
        const { ViewerComponent } = this.props;
        return (
            <ViewerComponent
                {...this.getViewerProps()}
                toggleExpansion={() => this.setIsExpanded(!this.getIsExpanded())}
            />
        );
    }

    renderExpandedViewer() {
        const { ViewerComponent } = this.props;
        if (ViewerComponent.Expanded) {
            return <ViewerComponent.Expanded {...this.getViewerProps()} />;
        }
        return null;
    }

    render() {
        return (
            <>
                <Highlightable
                    isHighlighted={this.state.isHighlighted}
                    onChange={(isHighlighted) => this.setIsHighlighted(isHighlighted)}
                    onKeyDown={(event) => this.onKeyDown(event)}
                >
                    <InputGroup>
                        {this.renderDragHandle()}
                        {this.renderBullet()}
                        <InputLine>{this.renderViewer()}</InputLine>
                        <Icon className="ml-1" title="Edit">
                            {this.renderEditButton()}
                        </Icon>
                        <Icon className="ml-1" title="Actions">
                            {this.renderActionsDropdown()}
                        </Icon>
                    </InputGroup>
                </Highlightable>
                {this.renderExpanded()}
            </>
        );
    }
}

BulletListItem.propTypes = {
    dataType: PropTypes.string.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    value: PropTypes.object.isRequired,
    valueKey: PropTypes.string.isRequired,
    ViewerComponent: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    viewerComponentProps: PropTypes.object,
    EditorComponent: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    prefixActions: PropTypes.array,

    // The following props are only used by BulletList.
    dragHandleSpace: PropTypes.bool,
    allowReordering: PropTypes.bool,
    onMoveUp: PropTypes.func,
    onMoveDown: PropTypes.func,
    isExpanded: PropTypes.bool,
    setIsExpanded: PropTypes.func,
};

BulletListItem.defaultProps = {
    prefixActions: [],
};

export default BulletListItem;


================================================
FILE: src/client/Common/BulletList/BulletListLine.js
================================================
import React from 'react';
import InputGroup from 'react-bootstrap/InputGroup';
import { GoPrimitiveDot } from 'react-icons/go';

function BulletListLine(props) {
    // eslint-disable-next-line react/prop-types
    const { children, ...moreProps } = props;
    return (
        <InputGroup {...moreProps}>
            <div className="icon" />
            <div className="icon mr-1">
                <GoPrimitiveDot />
            </div>
            {children}
        </InputGroup>
    );
}

export default BulletListLine;


================================================
FILE: src/client/Common/BulletList/BulletListPager.js
================================================
import PropTypes from 'prop-types';
import React from 'react';

import Highlightable from '../Highlightable';
import { KeyCodes } from '../Utils';
import BulletListLine from './BulletListLine';

class BulletListPager extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isHighlighted: false,
        };
    }

    onKeyDown(event) {
        if (event.keyCode === KeyCodes.SPACE) {
            this.props.updateLimit(this.props.limit + this.props.batchSize);
        } else if (event.keyCode === KeyCodes.ENTER) {
            this.props.updateLimit(null);
        }
    }

    renderButtons() {
        return (
            <>
                {' |'}
                <span
                    className="mx-1"
                    onClick={() => this.props.updateLimit(this.props.limit + this.props.batchSize)}
                >
                    Load More
                </span>
                |
                <span
                    className="mx-1"
                    onClick={() => this.props.updateLimit(null)}
                >
                    Load All
                </span>
            </>
        );
    }

    render() {
        let message;
        if (this.props.itemsLength === null) {
            if (this.props.limit === this.props.batchSize) {
                // We don't know whether we need pagination yet.
                return null;
            } if (this.props.limit) {
                message = `Fetching last ${this.props.limit} items ...`;
            } else {
                message = 'Fetching all items ...';
            }
            return (
                <BulletListLine className="pager">
                    {message}
                </BulletListLine>
            );
        }
        if (this.props.itemsLength <= this.props.batchSize && !this.props.hasMoreItems) {
            // No need for pagination.
            return null;
        }
        let buttons;
        if (this.props.hasMoreItems) {
            message = `Showing last ${this.props.itemsLength} items`;
            buttons = this.renderButtons();
        } else {
            message = `Showing all ${this.props.itemsLength} items`;
        }
        return (
            <Highlightable
                isHighlighted={this.state.isHighlighted}
                onChange={(isHighlighted) => this.setState({ isHighlighted })}
            >
                <BulletListLine className="pager">
                    {message}
                    {buttons}
                </BulletListLine>
            </Highlightable>
        );
    }
}

BulletListPager.propTypes = {
    batchSize: PropTypes.number.isRequired,
    limit: PropTypes.number,
    updateLimit: PropTypes.func.isRequired,
    itemsLength: PropTypes.number,
    hasMoreItems: PropTypes.bool,
};

export default BulletListPager;


================================================
FILE: src/client/Common/BulletList/BulletListTitle.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import InputGroup from 'react-bootstrap/InputGroup';
import { MdAddCircleOutline } from 'react-icons/md';
import { TiMinus, TiPlus } from 'react-icons/ti';

import Highlightable from '../Highlightable';
import { KeyCodes } from '../Utils';
import BulletListIcon from './BulletListIcon';

class BulletListTitle extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isHighlighted: false };
    }

    onKeyDown(event) {
        if (event.keyCode === KeyCodes.ENTER) {
            this.props.onAddButtonClick(event);
        }
    }

    renderListToggleButton() {
        if (this.props.areAllExpanded) {
            return (
                <BulletListIcon
                    title="Collapse All"
                    onClick={this.props.onToggleButtonClick}
                >
                    <TiMinus />
                </BulletListIcon>
            );
        }
        return (
            <BulletListIcon
                title="Expand All"
                onClick={this.props.onToggleButtonClick}
            >
                <TiPlus />
            </BulletListIcon>
        );
    }

    renderAddButton() {
        if (!this.props.onAddButtonClick) {
            return null;
        }
        return (
            <BulletListIcon
                title="Create New"
                onClick={this.props.onAddButtonClick}
            >
                <MdAddCircleOutline />
            </BulletListIcon>
        );
    }

    renderSortButton() {
        if (!this.props.onSortButtonClick) {
            return null;
        }
        // TODO: Use a proper icon to indicate sorting.
        // Was on a flight (no internet access) when I added this feature.
        return (
            <BulletListIcon
                title="Sort"
                onClick={this.props.onSortButtonClick}
            >
                <MdAddCircleOutline />
            </BulletListIcon>
        );
    }

    render() {
        return (
            <Highlightable
                isHighlighted={this.state.isHighlighted}
                onChange={(isHighlighted) => this.setState({ isHighlighted })}
                onKeyDown={(event) => this.onKeyDown(event)}
            >
                <InputGroup>
                    <div>{this.props.name}</div>
                    {this.state.isHighlighted ? this.renderListToggleButton() : null}
                    {this.state.isHighlighted ? this.renderAddButton() : null}
                    {this.state.isHighlighted ? this.renderSortButton() : null}
                </InputGroup>
            </Highlightable>
        );
    }
}

BulletListTitle.propTypes = {
    name: PropTypes.string.isRequired,
    areAllExpanded: PropTypes.bool.isRequired,
    onToggleButtonClick: PropTypes.func.isRequired,
    onAddButtonClick: PropTypes.func,
    onSortButtonClick: PropTypes.func,
};

export default BulletListTitle;


================================================
FILE: src/client/Common/BulletList/index.js
================================================
export { default } from './BulletList';


================================================
FILE: src/client/Common/ConfirmModal.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'react-bootstrap/Button';
import Modal from 'react-bootstrap/Modal';

import { suppressUnlessShiftKey } from './Utils';

function ConfirmModal(props) {
    return (
        <Modal
            show
            onHide={() => props.onClose()}
            onEscapeKeyDown={suppressUnlessShiftKey}
        >
            <Modal.Header closeButton>
                <Modal.Title>{props.title}</Modal.Title>
            </Modal.Header>
            <Modal.Body>
                {props.body}
            </Modal.Body>
            <Modal.Footer>
                <Button onClick={() => props.onClose(false)}>
                    {props.noLabel}
                </Button>
                {' '}
                <Button onClick={() => props.onClose(true)}>
                    {props.yesLabel}
                </Button>
            </Modal.Footer>
        </Modal>
    );
}

ConfirmModal.propTypes = {
    title: PropTypes.string.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    body: PropTypes.any.isRequired,
    yesLabel: PropTypes.string,
    noLabel: PropTypes.string,
    onClose: PropTypes.func.isRequired,
};

ConfirmModal.defaultProps = {
    yesLabel: 'Yes',
    noLabel: 'No',
};

export default ConfirmModal;


================================================
FILE: src/client/Common/Coordinator.js
================================================
const callbacks = {};

class Coordinator {
    static register(name, callback) {
        callbacks[name] = callback;
        return () => delete callbacks[name];
    }

    static invoke(name, ...args) {
        return callbacks[name].call(this, ...args);
    }

    static subscribe(name, callback) {
        if (!(name in callbacks)) {
            callbacks[name] = [];
        }
        callbacks[name].push(callback);
        return () => {
            const index = callbacks[name].indexOf(callback);
            callbacks[name].splice(index, 1);
        };
    }

    static broadcast(name, ...args) {
        if (!(name in callbacks)) {
            callbacks[name] = [];
        }
        callbacks[name].forEach((callback) => callback.call(this, ...args));
    }
}

export default Coordinator;


================================================
FILE: src/client/Common/DataLoader.js
================================================
import deepEqual from 'deep-equal';
import deepcopy from 'deepcopy';

import { getPartialItem, isItem } from '../../common/data_types';

function IGNORE() {
    return null;
}

class DataLoader {
    constructor({ getInput, onData, onError }) {
        this.getInput = getInput;
        this.input = null;
        this.cancelSubscription = null;
        this.onData = onData || IGNORE;
        this.onError = onError || IGNORE;
        this.isMounted = true;
        this.reload();
    }

    reload({ force } = {}) {
        const input = deepcopy(this.getInput());
        if (input && input.args && input.args.where) {
            // This is an optimization to prevent sending unnecessary data to the server.
            Object.entries(input.args.where).forEach(([key, value]) => {
                if (isItem(value)) {
                    input.args.where[key] = getPartialItem(value);
                }
            });
        }
        if (!force && deepEqual(input, this.input)) {
            return;
        }
        this.input = input;
        if (this.input === null) {
            this.onData(null);
            return;
        }
        window.api.send(this.input.name, this.input.args)
            .then((data) => {
                if (this.isMounted) {
                    this.setupSubscription();
                    this.onData(data);
                }
            })
            .catch((error) => {
                if (this.isMounted) {
                    this.onError(error);
                }
            });
    }

    // eslint-disable-next-line class-methods-use-this
    _compare(name, left, right) {
        if (name.endsWith('-load')) {
            return left.__id__ === right.__id__;
        } if (name.endsWith('-list')) {
            left = left.where || {};
            right = right.where || {};
            return Object.keys(left).every(
                (key) => typeof right[key] === 'undefined' || left[key] === right[key],
            );
        }
        return true;
    }

    setupSubscription() {
        const { promise, cancel } = window.api.subscribe(this.input.name);
        if (this.cancelSubscription) {
            this.cancelSubscription();
        }
        this.cancelSubscription = cancel;
        promise.then((data) => {
            if (!this.isMounted || !this.input) {
                return;
            }
            const queryArgs = this.input.args || {};
            const broadcastArgs = data || {};
            if (this._compare(this.input.name, queryArgs, broadcastArgs)) {
                this.reload({ force: true });
            } else {
                this.setupSubscription();
            }
        });
    }

    stop() {
        this.isMounted = false;
        if (this.cancelSubscription) {
            this.cancelSubscription();
        }
    }
}

export default DataLoader;


================================================
FILE: src/client/Common/DateContext.js
================================================
import React from 'react';

const DateContext = React.createContext(null);

DateContext.Wrapper = (Component) => (moreProps) => (
    <DateContext.Consumer>
        {(dateContext) => <Component {...dateContext} {...moreProps} />}
    </DateContext.Consumer>
);

export default DateContext;


================================================
FILE: src/client/Common/DatePicker.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import { Calendar } from 'react-date-range';

import DateUtils from '../../common/DateUtils';
import DateContext from './DateContext';
import PopoverElement from './PopoverElement';

// https://github.com/hypeserver/react-date-range
// Note: The corresponding CSS is included from DateRangePicker.

class DatePicker extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            lastDate: this.props.date || null,
        };
    }

    render() {
        const { todayLabel } = this.context;
        const lastDate = this.state.lastDate || todayLabel;
        return (
            <PopoverElement onReset={() => this.props.onChange(null)}>
                {this.props.date || 'Date: Unspecified'}
                <Calendar
                    date={DateUtils.getDate(this.props.date || lastDate)}
                    onChange={(rawDate) => {
                        const date = DateUtils.getLabel(rawDate);
                        this.setState({ lastDate: date });
                        this.props.onChange(date);
                    }}
                />
            </PopoverElement>
        );
    }
}

DatePicker.propTypes = {
    date: PropTypes.string,
    onChange: PropTypes.func.isRequired,
};

DatePicker.contextType = DateContext;

export default DatePicker;


================================================
FILE: src/client/Common/DateRangePicker.js
================================================
// https://adphorus.github.io/react-date-range/
import 'react-date-range/dist/styles.css'; // main css file
import 'react-date-range/dist/theme/default.css'; // theme css file

import React from 'react';
import { DateRangePicker as DateRangePickerOriginal } from 'react-date-range';

import DateUtils from '../../common/DateUtils';
import PropTypes from '../prop-types';
import DateContext from './DateContext';
import PopoverElement from './PopoverElement';

const KEY = 'selection';

function DateRangeSelector(props) {
    const { dateRange } = props;
    return (
        <DateRangePickerOriginal
            direction="horizontal"
            months={1}
            moveRangeOnFirstSelection={false}
            showSelectionPreview
            ranges={[
                {
                    key: KEY,
                    startDate: DateUtils.getDate(dateRange.startDate),
                    endDate: DateUtils.getDate(dateRange.endDate),
                },
            ]}
            onChange={(ranges) => props.onChange({
                startDate: DateUtils.getLabel(ranges[KEY].startDate),
                endDate: DateUtils.getLabel(ranges[KEY].endDate),
            })}
        />
    );
}

DateRangeSelector.propTypes = {
    dateRange: PropTypes.Custom.DateRange.isRequired,
    onChange: PropTypes.func.isRequired,
};

class DateRangePicker extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            lastDateRange: this.props.dateRange || null,
        };
    }

    renderSummary() {
        const { dateRange } = this.props;
        if (!dateRange) {
            return 'Date Range: Unspecified';
        }
        if (dateRange.startDate === dateRange.endDate) {
            return dateRange.startDate;
        }
        return `${dateRange.startDate} to ${dateRange.endDate}`;
    }

    render() {
        const { todayLabel } = this.context;
        const lastDateRange = this.state.lastDateRange || {
            startDate: todayLabel,
            endDate: todayLabel,
        };
        return (
            <PopoverElement onReset={() => this.props.onChange(null)}>
                {this.renderSummary()}
                <DateRangeSelector
                    dateRange={this.props.dateRange || lastDateRange}
                    onChange={(newDateRange) => {
                        this.setState({ lastDateRange: newDateRange });
                        this.props.onChange(newDateRange);
                    }}
                />
            </PopoverElement>
        );
    }
}

DateRangePicker.propTypes = {
    dateRange: PropTypes.Custom.DateRange,
    onChange: PropTypes.func.isRequired,
};

DateRangePicker.Selector = DateRangeSelector;

const DATE_RANGE_SEPARATOR = ' to ';

DateRangePicker.serialize = (dateRange) => {
    if (!dateRange) return null;
    return dateRange.startDate + DATE_RANGE_SEPARATOR + dateRange.endDate;
};

DateRangePicker.deserialize = (value) => {
    if (!value) return null;
    const [startDate, endDate] = value.split(DATE_RANGE_SEPARATOR);
    return { startDate, endDate };
};

DateRangePicker.contextType = DateContext;

export default DateRangePicker;


================================================
FILE: src/client/Common/Dropdown.css
================================================
.dropdown-toggle::after {
    display: none;
}

/**
 * There are multiple reports of this problem.
 * https://stackoverflow.com/q/42046287/903585
 * https://stackoverflow.com/q/18892351/903585
 * None of those solutions worked, so this is what I came up with.
 */
.dropdown-menu {
    inset: 0px 0px auto auto !important;
}


================================================
FILE: src/client/Common/Dropdown.js
================================================
import './Dropdown.css';

import PropTypes from 'prop-types';
import React from 'react';
import Dropdown from 'react-bootstrap/Dropdown';

import TypeaheadOptions from './TypeaheadOptions';

class CustomDropdown extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isShown: false };
    }

    componentDidMount() {
        if (Array.isArray(this.props.options)) {
            this.setState({ items: this.props.options });
        }
    }

    onSelect(item, event) {
        if (this.props.options instanceof TypeaheadOptions) {
            this.props.options.select(item)
                .then((adjustedItem) => {
                    // undefined = no change
                    // null = cancel operation
                    if (adjustedItem !== null) {
                        this.props.onChange(adjustedItem || item, event);
                    }
                });
        } else {
            this.props.onChange(item, event);
        }
    }

    setIsShown(nextIsShown) {
        if (nextIsShown) {
            this.show();
        } else {
            this.hide();
        }
    }

    hide() {
        this.setState({ isShown: false });
    }

    show() {
        if (this.props.options instanceof TypeaheadOptions) {
            this.props.options.search('')
                .then((items) => this.setState({ isShown: true, items }));
        } else {
            this.setState({ isShown: true });
        }
    }

    renderItems() {
        if (!this.state.items) return null;
        if (this.state.items.length === 0) {
            return (
                <Dropdown.Item disabled>
                    No Results
                </Dropdown.Item>
            );
        }
        return this.state.items.map((item) => (
            <Dropdown.Item
                key={item.__id__}
                onMouseDown={(event) => this.onSelect(item, event)}
            >
                {item[this.props.labelKey]}
            </Dropdown.Item>
        ));
    }

    render() {
        return (
            <Dropdown
                as="span"
                onToggle={(isShown) => this.setIsShown(isShown)}
                show={this.state.isShown}
            >
                <Dropdown.Toggle as="span">
                    {this.props.children}
                </Dropdown.Toggle>
                <Dropdown.Menu>
                    {this.renderItems()}
                </Dropdown.Menu>
            </Dropdown>
        );
    }
}

CustomDropdown.propTypes = {
    labelKey: PropTypes.string,
    // eslint-disable-next-line react/no-unused-prop-types
    disabled: PropTypes.bool.isRequired,
    options: PropTypes.oneOfType([
        PropTypes.instanceOf(TypeaheadOptions),
        PropTypes.array,
    ]).isRequired,
    onChange: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

CustomDropdown.defaultProps = {
    labelKey: 'name',
};

export default CustomDropdown;


================================================
FILE: src/client/Common/EditorModal.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'react-bootstrap/Button';
import InputGroup from 'react-bootstrap/InputGroup';
import Modal from 'react-bootstrap/Modal';

import LeftRight from './LeftRight';
import { debounce, KeyCodes, suppressUnlessShiftKey } from './Utils';

class EditorModal extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            value: props.value,
            status: 'Pending Validation ...',
            isSaving: false,
            isValidating: false,
        };
        this.validateItemDebounced = debounce(this.validateItemNotDebounced, 500);
    }

    componentDidMount() {
        this.validateItemDebounced();
    }

    onChange(value) {
        this.setState({ value }, () => this.validateItemDebounced());
    }

    onSave() {
        this.saveItemNotDebounced();
    }

    onClose() {
        this.props.onClose(this.state.value);
    }

    validateItemNotDebounced() {
        this.setState({ isValidating: true, status: 'Validating ...' });
        window.api.send(`${this.props.dataType}-validate`, this.state.value)
            .finally(() => this.setState({ isValidating: false }))
            .then((validationErrors) => this.setState({
                status: validationErrors.join('\n') || 'No validation errors!',
            }))
            .catch(() => this.setState({ status: 'Error!' }));
    }

    saveItemNotDebounced() {
        this.setState({ isSaving: true, status: 'Saving ...' });
        let promise;
        if (this.props.onSave) {
            // A custom onSave method is used for because reminder completion needs to
            // create the event and update the structure as part of same single transaction.
            promise = this.props.onSave(this.state.value);
            if (!(promise instanceof Promise)) {
                // If the custom onSave method does not return a promise,
                // it is assumed that the component will be unmounted.
                return;
            }
        } else {
            promise = window.api.send(`${this.props.dataType}-upsert`, this.state.value);
        }
        promise
            .finally(() => this.setState({ isSaving: false }))
            .then((value) => {
                this.setState({ status: 'Saved!', value });
                this.onClose();
            })
            .catch(() => this.setState({ status: 'Error!' }));
    }

    renderSaveButton() {
        return (
            <Button
                disabled={this.state.isSaving || this.state.isValidating}
                onClick={() => this.onSave()}
                style={{ width: '50px' }}
            >
                Save
            </Button>
        );
    }

    render() {
        if (!this.props.value) {
            return null;
        }
        const { EditorComponent, editorProps } = this.props;
        editorProps[this.props.valueKey] = this.state.value;
        editorProps.disabled = this.state.isSaving;
        return (
            <Modal
                show
                onHide={() => this.onClose()}
                onEscapeKeyDown={suppressUnlessShiftKey}
            >
                <Modal.Header closeButton>
                    <Modal.Title>Editor</Modal.Title>
                </Modal.Header>
                <Modal.Body>
                    <EditorComponent
                        {...editorProps}
                        onChange={(newValue) => this.onChange(newValue)}
                        onSpecialKeys={(event) => {
                            if (!event.shiftKey) return;
                            if (event.keyCode === KeyCodes.ENTER) {
                                this.onSave();
                            } else if (event.keyCode === KeyCodes.ESCAPE) {
                                this.onClose();
                            }
                        }}
                    />
                </Modal.Body>
                <Modal.Body>
                    <LeftRight>
                        <div style={{ whiteSpace: 'pre-wrap' }}>
                            {this.state.status}
                        </div>
                        <InputGroup>
                            {this.renderSaveButton()}
                        </InputGroup>
                    </LeftRight>
                </Modal.Body>
            </Modal>
        );
    }
}

EditorModal.propTypes = {
    dataType: PropTypes.string.isRequired,
    EditorComponent: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    valueKey: PropTypes.string.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    value: PropTypes.object.isRequired,
    onClose: PropTypes.func.isRequired, // provided by ModalStack

    // eslint-disable-next-line react/forbid-prop-types
    editorProps: PropTypes.object,
    onSave: PropTypes.func,
};

EditorModal.defaultProps = {
    editorProps: {},
};

export default EditorModal;


================================================
FILE: src/client/Common/EnumSelectorSection.js
================================================
import React from 'react';

import PropTypes from '../prop-types';
import LeftRight from './LeftRight';
import SidebarSection from './SidebarSection';

class EnumSelectorSection extends React.Component {
    renderOptions() {
        return this.props.options.map((option, index) => {
            let { label } = option;
            if (this.props.value !== option.value) {
                label = (
                    <a href="#" onClick={() => this.props.onChange(option.value)}>
                        {option.label}
                    </a>
                );
            }
            return (
                <span key={option.value}>
                    {index ? ' | ' : ''}
                    {' '}
                    {label}
                </span>
            );
        });
    }

    render() {
        return (
            <SidebarSection>
                <LeftRight>
                    <div className="mr-2">{this.props.label}</div>
                    <div>{this.renderOptions()}</div>
                </LeftRight>
            </SidebarSection>
        );
    }
}

EnumSelectorSection.propTypes = {
    label: PropTypes.string.isRequired,
    options: PropTypes.Custom.EnumOptions.isRequired,
    value: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
};

export default EnumSelectorSection;


================================================
FILE: src/client/Common/ErrorModal.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'react-bootstrap/Modal';

import { suppressUnlessShiftKey } from './Utils';

function ErrorModal(props) {
    let { error } = props;
    if (typeof error !== 'string') {
        error = JSON.stringify(error);
    }
    return (
        <Modal
            show
            onHide={props.onClose}
            onEscapeKeyDown={suppressUnlessShiftKey}
        >
            <Modal.Header closeButton>
                <Modal.Title>Error</Modal.Title>
            </Modal.Header>
            <Modal.Body>
                <pre>
                    {error}
                </pre>
            </Modal.Body>
        </Modal>
    );
}

ErrorModal.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    error: PropTypes.any.isRequired,
    onClose: PropTypes.func.isRequired,
};

export default ErrorModal;


================================================
FILE: src/client/Common/Highlightable.css
================================================

.highlightable:focus {
    outline: none;
}

.highlightable.highlighted {
    background: var(--component-highlight-color);
}


================================================
FILE: src/client/Common/Highlightable.js
================================================
import './Highlightable.css';

import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

import Coordinator from './Coordinator';

class Highlightable extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef();
    }

    componentDidMount() {
        this.deregisterCallbacks = [
            Coordinator.subscribe('unhighlight', () => this.setHighlight(false)),
        ];
    }

    componentWillUnmount() {
        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());
    }

    setHighlight(isHighlighted) {
        if (this.props.isHighlighted === isHighlighted) {
            return;
        }
        if (isHighlighted) {
            Coordinator.broadcast('unhighlight');
            this.ref.current.focus();
        }
        this.props.onChange(isHighlighted);
    }

    render() {
        const {
            isHighlighted, onChange: _, children, ...moreProps
        } = this.props;
        return (
            <div
                {...moreProps}
                className={classNames({
                    highlightable: true,
                    highlighted: isHighlighted,
                })}
                tabIndex={0}
                onMouseEnter={() => this.setHighlight(true)}
                onMouseLeave={() => this.setHighlight(false)}
                onFocus={() => this.setHighlight(true)}
                onBlur={() => this.setHighlight(false)}
                ref={this.ref}
            >
                {children}
            </div>
        );
    }
}

Highlightable.propTypes = {
    isHighlighted: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

export default Highlightable;


================================================
FILE: src/client/Common/Icon.css
================================================
.icon {
    cursor: pointer;
    height: 20px;
    position: relative;
    top: -1px;
    width: 13px;
}

.icon > svg {
    fill: var(--text-disabled-color);
}

.icon:not(.icon-never-highlight):hover > svg,
.icon.icon-highlighted > svg {
    fill: var(--text-color);
}


================================================
FILE: src/client/Common/Icon.js
================================================
import './Icon.css';

import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

function Icon(props) {
    const {
        alwaysHighlighted, neverHighlighted, className, children, ...moreProps
    } = props;
    moreProps.className = classNames({
        icon: true,
        'icon-highlighted': alwaysHighlighted,
        'icon-never-highlight': neverHighlighted,
    }, className);
    return (
        <div {...moreProps}>
            {children}
        </div>
    );
}

Icon.propTypes = {
    className: PropTypes.string,
    alwaysHighlighted: PropTypes.bool,
    neverHighlighted: PropTypes.bool,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

export default Icon;


================================================
FILE: src/client/Common/InfoModal.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'react-bootstrap/Modal';

import { suppressUnlessShiftKey } from './Utils';

function InfoModal(props) {
    return (
        <Modal
            show
            onHide={props.onClose}
            onEscapeKeyDown={suppressUnlessShiftKey}
        >
            <Modal.Header closeButton>
                <Modal.Title>{props.title}</Modal.Title>
            </Modal.Header>
            <Modal.Body>
                {props.message}
            </Modal.Body>
        </Modal>
    );
}

InfoModal.propTypes = {
    title: PropTypes.string.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    message: PropTypes.any.isRequired,
    onClose: PropTypes.func.isRequired,
};

export default InfoModal;


================================================
FILE: src/client/Common/InputLine.css
================================================
.input-line {
    flex: 1 1 auto;
    height: auto;
    overflow: hidden;
    width: 0px;
}

.input-line.overflow {
    overflow: visible;
}

.input-line.styled {
    background-color: var(--input-background-color);
}


================================================
FILE: src/client/Common/InputLine.js
================================================
import './InputLine.css';

import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

function InputLine(props) {
    const {
        className, overflow, styled, children, ...moreProps
    } = props;
    moreProps.className = classNames({
        'input-line': true,
        overflow,
        styled,
    }, className);
    return (
        <div {...moreProps}>
            {children}
        </div>
    );
}

InputLine.propTypes = {
    className: PropTypes.string,
    overflow: PropTypes.bool,
    styled: PropTypes.bool,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

export default InputLine;


================================================
FILE: src/client/Common/LeftRight.js
================================================
/* eslint-disable react/prop-types */

import React from 'react';

function LeftRight(props) {
    return (
        <div {...props}>
            <div className="d-flex">
                <div className="mr-auto">{props.children[0]}</div>
                <div>{props.children[1]}</div>
            </div>
        </div>
    );
}

export default LeftRight;


================================================
FILE: src/client/Common/Link.js
================================================
import assert from 'assert';
import React from 'react';

import PropTypes from '../prop-types';
import Coordinator from './Coordinator';

function Link(props) {
    const { logStructure, logTopic } = props;
    assert(!(logStructure && logTopic));
    const item = logStructure || logTopic;
    assert(item);

    let link;
    try {
        link = Coordinator.invoke('url-link', { details: item });
    } catch (error) {
        link = '#';
    }
    return (
        <a
            className="topic"
            title={item.name}
            href={link}
            tabIndex={-1}
            onClick={(event) => {
                event.preventDefault();
                event.stopPropagation();
                Coordinator.invoke('url-update', { details: item });
            }}
        >
            {props.children}
        </a>
    );
}

Link.propTypes = {
    logStructure: PropTypes.Custom.LogStructure,
    logTopic: PropTypes.Custom.LogTopic,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

export default Link;


================================================
FILE: src/client/Common/ModalStack.js
================================================
import assert from 'assert';
import React from 'react';

import ConfirmModal from './ConfirmModal';
import Coordinator from './Coordinator';
import EditorModal from './EditorModal';
import ErrorModal from './ErrorModal';
import InfoModal from './InfoModal';

class ModalStack extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            components: [],
            sourceElement: null,
        };
    }

    componentDidMount() {
        this.deregisterCallbacks = [
            Coordinator.register(
                'modal-editor',
                (componentProps) => this.push(EditorModal, componentProps),
            ),
            Coordinator.register('modal-confirm', this.push.bind(this, ConfirmModal)),
            Coordinator.register('modal-error', (error) => this.push(ErrorModal, { error })),
            Coordinator.register('modal-info', ({ title, message }) => this.push(InfoModal, { title, message })),
        ];
    }

    componentWillUnmount() {
        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());
    }

    push(ComponentClass, componentProps) {
        const index = this.state.components.length;
        this.setState((state) => {
            if (index === 0) {
                state.sourceElement = document.activeElement;
            }
            state.components.push({ ComponentClass, componentProps });
            return state;
        });
        return this.pop.bind(this, index);
    }

    pop(index, callback) {
        this.setState((state) => {
            state.components.pop();
            assert(index === state.components.length);
            if (index === 0) {
                state.sourceElement.focus();
                state.sourceElement = null;
            }
            return state;
        }, callback);
    }

    renderItem({ ComponentClass, componentProps }, index) {
        return (
            <ComponentClass
                key={index}
                {...componentProps}
                onClose={(...args) => this.pop(index, () => {
                    if (componentProps.onClose) {
                        componentProps.onClose(...args);
                    }
                })}
            />
        );
    }

    render() {
        return this.state.components.map((item, index) => this.renderItem(item, index));
    }
}

export default ModalStack;


================================================
FILE: src/client/Common/Plugins.js
================================================
/* eslint-disable max-classes-per-file */

import React from 'react';

import { Enum } from '../../common/data_types';
import PropTypes from '../prop-types';
import SettingsContext from './SettingsContext';

export class PluginClient {
    static getSettingsKey() {
        // The key corresponding to the setting for your plugin.
        // Must be unique across all plugins.
        // Maybe infer this based on path?
        throw new Error('not implemented');
    }

    static getSettingsComponent() {
        // Return a React element that is rendered in the SettingsEditor.
        // Props = { disabled: bool, value: any, onChange: function }
        throw new Error('not implemented');
    }

    static getDisplayLocation() {
        // A string that indicated where this component should be rendered.
        // The various options can be found in Application.js
        throw new Error('not implemented');
    }

    static getDisplayComponent() {
        // Return a React element that is rendered in the application UI.
        // Gets the "setting" as property.
        throw new Error('not implemented');
    }

    static getTabData() {
        // Return an object that contains data about an extra Tab.
        // { value: string, label: string }
        throw new Error('not implemented');
    }
}

export const PluginDisplayLocation = Enum([
    {
        value: 'tab_section',
    },
    {
        value: 'right_sidebar_main_top',
    },
    {
        value: 'right_sidebar_main_bottom',
    },
    {
        value: 'right_sidebar_widgets_top',
    },
    {
        value: 'right_sidebar_widgets_bottom',
    },
]);

export class PluginDisplayComponent extends React.Component {
    renderActual(settings) {
        const results = [];
        Object.entries(this.props.plugins).forEach(([name, api]) => {
            if (api.getDisplayLocation() !== this.props.location) {
                return;
            }
            const key = api.getSettingsKey();
            const props = {
                settings: key ? settings[key] : null,
            };
            results.push(<div key={`plugin:${name}`}>{api.getDisplayComponent(props)}</div>);
        });
        return results;
    }

    render() {
        return (
            <SettingsContext.Consumer>
                {(settings) => this.renderActual(settings)}
            </SettingsContext.Consumer>
        );
    }
}

PluginDisplayComponent.propTypes = {
    plugins: PropTypes.Custom.Plugins.isRequired,
    location: PropTypes.string.isRequired,
};


================================================
FILE: src/client/Common/PopoverElement.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'react-bootstrap/Button';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { MdClose } from 'react-icons/md';

import InputLine from './InputLine';

class PopoverElement extends React.Component {
    renderOverlayTrigger() {
        const overlay = (
            <Popover id="date-range-selector">
                {this.props.children[1]}
            </Popover>
        );
        return (
            <OverlayTrigger
                trigger="click"
                rootClose
                placement="bottom-start"
                overlay={overlay}
            >
                <InputLine styled className="px-1">
                    {this.props.children[0]}
                </InputLine>
            </OverlayTrigger>
        );
    }

    renderButton() {
        return <Button onClick={() => this.props.onReset(null)}><MdClose /></Button>;
    }

    render() {
        return (
            <>
                {this.renderOverlayTrigger()}
                {this.renderButton()}
            </>
        );
    }
}

PopoverElement.propTypes = {
    onReset: PropTypes.func.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any.isRequired,
};

export default PopoverElement;


================================================
FILE: src/client/Common/ScrollableSection.css
================================================
.scrollable-section {
    padding-right: 4px;
    overflow-y: scroll;
}

.scrollable-section::-webkit-scrollbar {
    width: 8px;
}

.scrollable-section::-webkit-scrollbar-track {
    background: var(--background-color);
    border-radius: 4px;
}

.scrollable-section::-webkit-scrollbar-thumb {
    background-color: var(--component-color);
    border-radius: 4px;
}


================================================
FILE: src/client/Common/ScrollableSection.js
================================================
/* eslint-disable max-classes-per-file */

import './ScrollableSection.css';

import PropTypes from 'prop-types';
import React from 'react';

class WindowHeightDetector {
    static subscribe(callback) {
        if (!WindowHeightDetector.instance) {
            WindowHeightDetector.instance = new WindowHeightDetector();
        }
        const { instance } = WindowHeightDetector;
        instance.callbacks.push(callback);
        return instance.height;
    }

    constructor() {
        this.callbacks = [];
        this.height = window.innerHeight;
        window.addEventListener('resize', this.onResize.bind(this));
    }

    onResize() {
        this.height = window.innerHeight;
        this.callbacks.forEach((callback) => callback(this.height));
    }
}

class ScrollableSection extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            height: WindowHeightDetector.subscribe((height) => this.setState({ height })),
        };
    }

    render() {
        const height = this.state.height
            - this.props.padding
            - 32; // Why 32? 16px padding at top/bottom.
        return (
            <div className="scrollable-section" style={{ height }}>
                {this.props.children}
            </div>
        );
    }
}

ScrollableSection.propTypes = {
    padding: PropTypes.number,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

ScrollableSection.defaultProps = {
    padding: 0,
};

export default ScrollableSection;


================================================
FILE: src/client/Common/Selector.js
================================================
/* eslint-disable max-classes-per-file */

import PropTypes from 'prop-types';
import React from 'react';
import Form from 'react-bootstrap/Form';

class Selector extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef();
    }

    focus() {
        this.ref.current.focus();
    }

    render() {
        const { onChange, options, ...moreProps } = this.props;
        return (
            <Form.Control
                {...moreProps}
                className="selector"
                as="select"
                onChange={(event) => onChange(event.target.value)}
                ref={this.ref}
            >
                {options.map((item) => {
                    const optionProps = { key: item.value, value: item.value };
                    return <option {...optionProps}>{item.label}</option>;
                })}
            </Form.Control>
        );
    }
}

Selector.propTypes = {
    value: PropTypes.string.isRequired,
    options: PropTypes.arrayOf(
        PropTypes.shape({
            label: PropTypes.string.isRequired,
            value: PropTypes.string.isRequired,
        }),
    ).isRequired,
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
};

Selector.getStringListOptions = (items) => items.map((item) => ({
    label: item,
    value: item,
}));

class BinarySelector extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef();
    }

    focus() {
        this.ref.current.focus();
    }

    render() {
        const {
            noLabel, yesLabel, value, onChange, ...moreProps
        } = this.props;
        const options = [
            { label: noLabel, value: 'no' },
            { label: yesLabel, value: 'yes' },
        ];
        return (
            <Selector
                {...moreProps}
                value={options[value ? 1 : 0].value}
                options={options}
                onChange={(newValue) => onChange(newValue === options[1].value)}
                ref={this.ref}
            />
        );
    }
}

BinarySelector.propTypes = {
    value: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
    noLabel: PropTypes.string,
    yesLabel: PropTypes.string,
};

BinarySelector.defaultProps = {
    noLabel: 'No',
    yesLabel: 'Yes',
};

Selector.Binary = BinarySelector;

export default Selector;


================================================
FILE: src/client/Common/SettingsContext.js
================================================
import React from 'react';

const SettingsContext = React.createContext({});

SettingsContext.Wrapper = (Component) => (moreProps) => (
    <SettingsContext.Consumer>
        {(settings) => <Component settings={settings} {...moreProps} />}
    </SettingsContext.Consumer>
);

export default SettingsContext;


================================================
FILE: src/client/Common/SidebarSection.css
================================================
.sidebar-section {
    border: 1px solid var(--background-color);
    border-radius: 4px;
    padding: 3px 8px 5px;
    margin: 4px 0;
}

.sidebar-section:hover {
    border-color: var(--component-highlight-color);
}

.sidebar-section.selected {
    background: var(--component-color);
    border-color: var(--component-highlight-color);
}

.sidebar-section > .header {
    color: var(--text-disabled-color);
}

.sidebar-section > .cursor {
    cursor: pointer;
}

.sidebar-section > .separator {
    border-bottom: 1px solid var(--component-highlight-color);
    margin-bottom: 4px;
    padding-bottom: 4px;
}

.sidebar-section > li {
    width: 1000px;
}

.sidebar-section > li > a {
    left: -8px;
    position: relative;
}


================================================
FILE: src/client/Common/SidebarSection.js
================================================
import './SidebarSection.css';

import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import InputGroup from 'react-bootstrap/InputGroup';
import { GoPrimitiveDot } from 'react-icons/go';
import { TiMinus, TiPlus } from 'react-icons/ti';

import Icon from './Icon';
import LeftRight from './LeftRight';

class SidebarSection extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isCollapsed: false };
    }

    renderHeader() {
        if (!this.props.title) {
            return null;
        }
        const { isCollapsed } = this.state;
        return (
            <LeftRight
                className={classNames({
                    header: true,
                    cursor: true,
                    separator: !isCollapsed,
                })}
                onClick={() => this.setState({ isCollapsed: !isCollapsed })}
            >
                {this.props.title}
                <Icon>{isCollapsed ? <TiPlus /> : <TiMinus />}</Icon>
            </LeftRight>
        );
    }

    renderChildren() {
        if (this.state.isCollapsed) {
            return null;
        }
        return (
            <div className={classNames({ cursor: !this.props.title })}>
                {this.props.children}
            </div>
        );
    }

    render() {
        const {
            selected, title: _title, children: _children, ...moreProps
        } = this.props;
        return (
            <div
                {...moreProps}
                className={classNames({
                    'sidebar-section': true,
                    selected,
                })}
            >
                {this.renderHeader()}
                {this.renderChildren()}
            </div>
        );
    }
}

SidebarSection.propTypes = {
    selected: PropTypes.bool,
    // eslint-disable-next-line react/forbid-prop-types
    title: PropTypes.any,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

SidebarSection.Item = function ({ children }) {
    return (
        <InputGroup>
            <Icon alwaysHighlighted className="mr-1">
                <GoPrimitiveDot />
            </Icon>
            {children}
        </InputGroup>
    );
};

SidebarSection.Item.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

export default SidebarSection;


================================================
FILE: src/client/Common/SortableList.css
================================================
button.sortableDragHandle.btn {
    cursor: grab;
}

.sortableDraggedItem {
    cursor: grab;
    z-index: 10000;
}


================================================
FILE: src/client/Common/SortableList.js
================================================
import './SortableList.css';

import arrayMove from 'array-move';
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'react-bootstrap/Button';
import { GoTrashcan } from 'react-icons/go';
import { GrDrag } from 'react-icons/gr';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';

const SortableDragHandle = SortableHandle((props) => (
    <Button
        className="sortableDragHandle"
        disabled={props.disabled}
    >
        <GrDrag />
    </Button>
));

const WrappedContainer = SortableContainer(({ children }) => <div>{children}</div>);

const WrappedRow = SortableElement((props) => {
    const disabled = props.wrappedRowDisabled;
    const { children, ...otherProps } = props.originalElement.props;
    return React.createElement(
        props.originalElement.type,
        otherProps,
        [
            <SortableDragHandle
                key="drag"
                disabled={disabled}
                title="Reorder"
            />,
            ...(children || []),
            <Button
                key="delete"
                disabled={disabled}
                onClick={props.onDelete}
                title="Delete"
            >
                <GoTrashcan />
            </Button>,
        ],
    );
});

class SortableList extends React.Component {
    constructor(props) {
        super(props);
        let { type } = props;
        if (type.constructor) {
            type = (innerProps) => React.createElement(props.type, innerProps);
        }
        this.state = { type };
    }

    onReorder({ oldIndex, newIndex }) {
        this.props.onChange(arrayMove(this.props.items, oldIndex, newIndex));
    }

    onChange(index, item) {
        const items = [...this.props.items];
        items[index] = item;
        this.props.onChange(items);
    }

    onDelete(index) {
        const items = [...this.props.items];
        items.splice(index, 1);
        this.props.onChange(items);
    }

    renderRow(item, index) {
        const {
            items, itemsKey, onChange: _onChange, type: _type, disabled, ...moreProps
        } = this.props;
        return React.createElement(WrappedRow, {
            key: item.__id__,
            // Consumed by SortableElement
            index,
            disabled,
            // Forwarded to the WrappedRow.
            originalElement: this.state.type({
                disabled,
                onChange: (updatedItem) => this.onChange(index, updatedItem),
                [itemsKey]: items,
                index,
                ...moreProps,
            }),
            wrappedRowDisabled: disabled,
            onDelete: () => this.onDelete(index),
        });
    }

    render() {
        return (
            <WrappedContainer
                helperClass="sortableDraggedItem"
                useDragHandle
                onSortEnd={(data) => this.onReorder(data)}
            >
                {this.props.items.map((item, index) => this.renderRow(item, index))}
            </WrappedContainer>
        );
    }
}

SortableList.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    items: PropTypes.arrayOf(PropTypes.any.isRequired).isRequired,
    onChange: PropTypes.func.isRequired,
    type: PropTypes.func.isRequired,
    itemsKey: PropTypes.string.isRequired,
    disabled: PropTypes.bool.isRequired,
};

export default SortableList;


================================================
FILE: src/client/Common/StandardIcons.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import { AiOutlineWarning } from 'react-icons/ai';
import { BiDetail } from 'react-icons/bi';
import { MdHelp, MdInfo } from 'react-icons/md';

function getIconWrapper(Component, color = 'var(--link-color)', style = { cursor: 'pointer' }) {
    function IconWrapper(props) {
        const { isShown, ...moreProps } = props;
        if (!isShown) {
            return null;
        }
        return (
            <Component
                className="ml-1"
                color={color}
                style={style}
                {...moreProps}
            />
        );
    }
    IconWrapper.propTypes = {
        isShown: PropTypes.bool.isRequired,
    };
    return IconWrapper;
}

export const WarningIcon = getIconWrapper(AiOutlineWarning, 'var(--warning-color)', { position: 'relative', top: -1 });
export const DetailsIcon = getIconWrapper(BiDetail);
export const HelpIcon = getIconWrapper(MdHelp);
export const InfoIcon = getIconWrapper(MdInfo);


================================================
FILE: src/client/Common/TextEditor.css
================================================
.public-DraftStyleDefault-ul,
.public-DraftStyleDefault-ol {
    margin: 0;
}

.text-editor {
    position: relative;
}

.text-editor.min-width .public-DraftEditor-content {
    min-width: 100px;
}

.text-editor.styled {
    background: var(--input-background-color);
    color: var(--input-text-color);
}

.text-editor.styled .public-DraftEditor-content {
    background: var(--input-background-color);
    border-radius: 0 0.2rem 0.2rem 0;
    color: var(--input-text);
    min-height: 20px;
    padding: 1px 4px 0;
}

.text-editor.styled.disabled .public-DraftEditor-content {
    background: var(--input-disabled-background-color);
}

.text-editor.isSingleLine {
    display: inline-block;
}

/* TextEditor Mention Suggestions + Dropdown Options (used by RBT Suggestions) */

.dropdown-menu,
.text-editor .mention-suggestions > div {
    background-color: var(--component-color);
    border: 1px solid var(--component-highlight-color);
    box-shadow: 0px 4px 30px 0px var(--component-color);
    font-family: var(--font-family);
    font-size: var(--font-size);
    min-width: 100px;
    position: absolute;
    z-index: 2;
}

.dropdown-item,
.text-editor .mention-suggestions > div > div {
    color: var(--text-color);
    padding: 1px 16px;
}

.text-editor .mention-suggestions > div > div > span {
    font-size: inherit;
    margin: 0;
    overflow: inherit;
}

.dropdown-item.active,
.text-editor .mention-suggestions > div > div[aria-selected="true"] {
    background-color: var(--suggestion-highlight-color);
}


================================================
FILE: src/client/Common/TextEditor.js
================================================
import 'draft-js/dist/Draft.css';
import './TextEditor.css';

import assert from 'assert';
import classNames from 'classnames';
import { RichUtils } from 'draft-js';
import createMarkdownShortcutsPlugin from 'draft-js-markdown-shortcuts-plugin';
import createMentionPlugin from 'draft-js-mention-plugin';
import Editor from 'draft-js-plugins-editor';
import PropTypes from 'prop-types';
import React from 'react';

import RichTextUtils from '../../common/RichTextUtils';
import AddLinkPlugin from './AddLinkPlugin';
import Link from './Link';
import TypeaheadOptions from './TypeaheadOptions';
import { KeyCodes } from './Utils';

function MentionComponent(props) {
    return <Link logTopic={props.mention}>{props.children}</Link>;
}

MentionComponent.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    mention: PropTypes.any.isRequired,
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any,
};

function OptionComponent(props) {
    const {
        isFocused: _isFocused, // eslint-disable-line react/prop-types
        mention: item,
        searchValue: _searchValue, // eslint-disable-line react/prop-types
        theme: _theme, // eslint-disable-line react/prop-types
        ...moreProps
    } = props;
    return <div {...moreProps}>{item.name}</div>;
}

OptionComponent.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    mention: PropTypes.any.isRequired,
};

class TextEditor extends React.Component {
    static getDerivedStateFromProps(props, state) {
        if (state.onChange) {
            return { onChange: false };
        }
        const isFirstTime = !('value' in state);
        // WARNING: Even if props.value is equivalent to state.value, they might
        // not be in the same format, and that could lead to an infinite loop!
        if (isFirstTime || !RichTextUtils.equals(state.value, props.value)) {
            return {
                value: props.value,
                editorState: RichTextUtils.toEditorState(props.value),
            };
        }
        return null;
    }

    constructor(props) {
        super(props);
        this.state = {
            suggestions: [],
            open: false,
            plugins: [],
        };

        this.state.plugins.push(AddLinkPlugin);

        if (!this.props.isSingleLine) {
            this.markdownShortcutsPlugin = createMarkdownShortcutsPlugin();
            this.state.plugins.push(this.markdownShortcutsPlugin);
        }

        this.mentionPlugin = createMentionPlugin({
            mentionComponent: MentionComponent,
            supportWhitespace: true,
        });
        // Workaround for two bugs in draft-js-mention-plugin v3.x:
        // 1. Deleting @ doesn't close the dropdown (early return skips closeDropdown).
        // 2. Typing @ at a non-zero offset doesn't trigger search (> vs >=).
        // Detection: onSearchChange fires synchronously inside pluginOnChange
        // when the plugin finds an active search. If it didn't fire, either
        // the search ended (clear suggestions) or was skipped (trigger manually).
        const pluginOnChange = this.mentionPlugin.onChange;
        this.mentionPlugin.onChange = (editorState) => {
            this._searchFired = false;
            const result = pluginOnChange(editorState);
            if (!this._searchFired) {
                if (this.state.open) {
                    this.setState({ suggestions: [] });
                } else if (this.props.options) {
                    const text = editorState.getCurrentContent().getPlainText();
                    const offset = editorState.getSelection().getAnchorOffset();
                    if (offset > 0 && text.charAt(offset - 1) === '@') {
                        this.onSearchChange({ value: '' });
                    }
                }
            }
            return result;
        };
        this.state.plugins.push(this.mentionPlugin);

        this.ref = React.createRef();
    }

    handleKeyCommand(command, editorState) {
        const newState = RichUtils.handleKeyCommand(editorState, command);
        if (newState) {
            this.onChange(newState);
            return 'handled';
        }
        if (this.props.isSingleLine && command === 'split-block') {
            return 'handled';
        }
        return 'not-handled';
    }

    onSearchChange({ value: query }) {
        this._searchFired = true;
        this.props.options
            .search(query)
            .then((suggestions) => this.setState({ suggestions }));
    }

    onAddMention(option) {
        this.props.options
            .select(option)
            .then((result) => {
                if (typeof result === 'undefined') return;
                const selection = RichTextUtils.getSelectionData(this.state.editorState);
                // Abstraction leak! Do not assume name.
                const delta = result.name.length - option.name.length;
                selection.anchorOffset += delta;
                selection.focusOffset += delta;
                let content = RichTextUtils.fromEditorState(this.state.editorState);
                content = RichTextUtils.updateDraftContent(content, [option], [result || '']);
                let editorState = RichTextUtils.toEditorState(content);
                // TODO: Figure out why the cursor is not updated properly.
                editorState = RichTextUtils.setSelectionData(editorState, selection);
                this.onChange(editorState);
            });
    }

    onChange(editorState) {
        editorState = RichTextUtils.fixCursorBug(this.state.editorState, editorState);
        this.setState({ editorState });
        const newValue = RichTextUtils.fromEditorState(editorState);
        if (this.props.onChange) {
            this.setState(
                { onChange: true, value: newValue },
                () => this.props.onChange(newValue),
            );
        }
    }

    focus() {
        // Why the delay?
        // This broke something inside the DraftJS Editor
        // that caused mentions to not be rendered properly.
        window.setTimeout(this.ref.current.focus, 0);
    }

    keyBindingFn(event) {
        if (
            this.props.isSingleLine
            && [KeyCodes.ESCAPE, KeyCodes.ENTER].includes(event.keyCode)
            && this.props.onSpecialKeys
        ) {
            this.props.onSpecialKeys(event);
        }
        // https://github.com/draft-js-plugins/draft-js-plugins/issues/1117
        // Do not invoke getDefaultKeyBinding here!
    }

    renderSuggestions() {
        const { MentionSuggestions } = this.mentionPlugin;
        const suggestions = this.state.suggestions
            .map((item) => ({ id: `${item.__type__}:${item.__id__}`, ...item }));
        return (
            <div className="mention-suggestions">
                <MentionSuggestions
                    onOpen={() => this.setState({ open: true })}
                    onClose={() => this.setState({ open: false })}
                    onSearchChange={(data) => this.onSearchChange(data)}
                    onAddMention={(option) => this.onAddMention(option)}
                    suggestions={suggestions}
                    entryComponent={OptionComponent}
                />
            </div>
        );
    }

    render() {
        return (
            <div className={classNames({
                'text-editor': true,
                unstyled: this.props.unstyled,
                styled: !this.props.unstyled,
                disabled: this.props.disabled,
                isSingleLine: this.props.isSingleLine,
                'min-width': this.props.minWidth,
            })}
            >
                <Editor
                    readOnly={this.props.disabled}
                    editorState={this.state.editorState}
                    keyBindingFn={(event) => this.keyBindingFn(event)}
                    handleKeyCommand={
                        (command, editorState) => this.handleKeyCommand(command, editorState)
                    }
                    plugins={this.state.plugins}
                    onChange={(editorState) => this.onChange(editorState)}
                    placeholder={this.props.placeholder}
                    ref={this.ref}
                />
                {this.props.disabled ? null : this.renderSuggestions()}
            </div>
        );
    }
}

TextEditor.propTypes = {
    unstyled: PropTypes.bool,
    disabled: PropTypes.bool,
    placeholder: PropTypes.string,
    minWidth: PropTypes.bool,

    // eslint-disable-next-line react/forbid-prop-types
    value: PropTypes.object,
    onChange: PropTypes.func,

    isSingleLine: PropTypes.bool,
    onSpecialKeys: PropTypes.func,

    options: PropTypes.instanceOf(TypeaheadOptions),
};

TextEditor.defaultProps = {
    unstyled: false,
    disabled: false,
    isSingleLine: false,
    minWidth: false,
};

/**
 * The primary component generates an inline block that does not look great on
 * the main page. This alternative implementation generates an inline span.
 */
TextEditor.SimpleViewer = function (props) {
    if (!props.value) {
        return null;
    }
    const rawContent = props.value;
    assert(rawContent.blocks.length === 1);
    const { text } = rawContent.blocks[0];
    let textIndex = 0;
    const entityRanges = Object.values(rawContent.blocks[0].entityRanges);
    let entityRangeIndex = 0;
    const parts = [];
    while (textIndex < text.length) {
        const entityRange = entityRanges[entityRangeIndex];
        let part;
        if (entityRange && entityRange.offset === textIndex) {
            const key = `entity-${entityRangeIndex}`;
            entityRangeIndex += 1;
            const entity = rawContent.entityMap[entityRange.key];
            const endIndex = textIndex + entityRange.length;
            const textPart = text.slice(textIndex, endIndex);
            textIndex = endIndex;
            if (entity.type === 'mention') {
                part = (
                    <Link key={key} logTopic={entity.data.mention}>
                        {textPart}
                    </Link>
                );
            } else if (entity.type === 'LINK') {
                part = (
                    <a key={key} href={entity.data.url} target="_blank" rel="noopener noreferrer">
                        {textPart}
                    </a>
                );
            } else {
                assert(false, `unknown entity type: ${entity.type}`);
            }
        } else {
            const endIndex = entityRange ? entityRange.offset : text.length;
            const textPart = text.slice(textIndex, endIndex);
            textIndex = endIndex;
            part = textPart;
        }
        parts.push(part);
    }
    return <span>{parts}</span>;
};

TextEditor.SimpleViewer.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    value: PropTypes.any,
};

export default TextEditor;


================================================
FILE: src/client/Common/TextInput.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import Form from 'react-bootstrap/Form';

class TextInput extends React.Component {
    constructor(props) {
        super(props);
        this.ref = React.createRef();
    }

    focus() {
        this.ref.current.focus();
    }

    render() {
        const {
            value, disabled, onChange, ...moreProps
        } = this.props;
        return (
            <Form.Control
                value={value}
                disabled={disabled}
                onChange={(event) => onChange(event.target.value)}
                ref={this.ref}
                {...moreProps}
            />
        );
    }
}

TextInput.propTypes = {
    value: PropTypes.string.isRequired,
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
};

export default TextInput;


================================================
FILE: src/client/Common/TooltipElement.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';

function TooltipElement(props) {
    const overlay = (
        <Tooltip style={{ width: 200 }}>
            {props.children[1]}
        </Tooltip>
    );
    return (
        <OverlayTrigger
            rootClose
            placement="right-start"
            overlay={overlay}
        >
            {props.children[0]}
        </OverlayTrigger>
    );
}

TooltipElement.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    children: PropTypes.any.isRequired,
};

export default TooltipElement;


================================================
FILE: src/client/Common/TypeaheadInput.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import { AsyncTypeahead } from 'react-bootstrap-typeahead';

class TypeaheadInput extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isLoading: false, options: [] };
        this.ref = React.createRef();
    }

    onSearch(query) {
        this.setState({ isLoading: true }, () => {
            this.props.onSearch(query)
                .then((options) => this.setState({ isLoading: false, options }));
        });
    }

    focus() {
        this.ref.current.focus();
    }

    render() {
        return (
            <AsyncTypeahead
                id={this.props.id}
                minLength={0}
                disabled={this.props.disabled}
                onFocus={() => this.onSearch(this.props.value)}
                onSearch={(query) => this.onSearch(query)}
                filterBy={this.props.filterBy}
                placeholder={this.props.placeholder}
                selected={[this.props.value]}
                onInputChange={(newValue) => {
                    this.onSearch(newValue);
                    this.props.onChange(newValue);
                }}
                onChange={(newSelected) => {
                    if (newSelected.length) {
                        this.props.onChange(newSelected[0]);
                    }
                }}
                renderMenuItemChildren={(option) => <div>{option}</div>}
                isLoading={this.state.isLoading}
                options={this.state.options}
                ref={this.ref}
            />
        );
    }
}

TypeaheadInput.propTypes = {
    id: PropTypes.string.isRequired,
    value: PropTypes.string.isRequired,
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
    onSearch: PropTypes.func.isRequired,

    placeholder: PropTypes.string,
    filterBy: PropTypes.func,
};

export default TypeaheadInput;


================================================
FILE: src/client/Common/TypeaheadOptions.js
================================================
import assert from 'assert';

class TypeaheadOptions {
    static getFromTypes(names) {
        return new TypeaheadOptions({
            serverSideOptions: names.map((name) => ({ name })),
        });
    }

    constructor(config) {
        assert(Array.isArray(config.serverSideOptions));
        if (!config.prefixOptions) {
            config.prefixOptions = [];
        }
        if (!config.suffixOptions) {
            config.suffixOptions = [];
        }
        if (!config.getComputedOptions) {
            config.getComputedOptions = async () => [];
        }
        if (!config.computedOptionTypes) {
            config.computedOptionTypes = [];
        }
        if (!config.allowMultipleItems) {
            config.allowMultipleItems = {};
            config.allowMultipleItems['log-topic'] = true;
        }
        this.config = config;
    }

    async search(query, existingItems) {
        const skipTypes = {};
        if (existingItems) {
            // When existing items are provided, we can check to see which types
            // have already been selected, and exclude them from the results.
            existingItems.forEach((item) => {
                if (!this.config.allowMultipleItems[item.__type__]) {
                    skipTypes[item.__type__] = true;
                }
            });
        }
        // Server-side filtering invokes case insensitive LIKE `${query}%`.
        let options = await Promise.all(
            this.config.serverSideOptions
                .filter((item) => !skipTypes[item.name])
                .map((item) => window.api.send(
                    `${item.name}-typeahead`,
                    { query, where: item.where },
                )),
        );
        options = options.flat();

        const doesMatchQuery = (item) => item.name.toLowerCase().startsWith(query.toLowerCase());
        options = [
            ...this.config.prefixOptions
                .filter((item) => !skipTypes[item.__type__])
                .filter(doesMatchQuery),
            ...options,
            ...this.config.suffixOptions
                .filter((item) => !skipTypes[item.__type__])
                .filter(doesMatchQuery),
        ];
        if (this.config.serverSideOptions.length > 1) {
            const seenOptionIds = new Set();
            // Since option.__id__ is used as a React Array Key, adjust it.
            // Do this only if needed to minimize later adjustment.
            options.forEach((option) => {
                if (seenOptionIds.has(option.__id__)) {
                    option.__original_id__ = option.__id__;
                    option.__id__ = `${option.__type__}:${option.__id__}`;
                } else {
                    seenOptionIds.add(option.__id__);
                }
            });
        }
        const computedOptions = await this.config.getComputedOptions(query);
        options.push(...computedOptions);
        // TODO: Maybe prefix type name, before item name, for clarity.
        return options;
    }

    async select(option) {
        let adjusted = false;
        if (option.__original_id__) {
            option.__id__ = option.__original_id__;
            delete option.__original_id__;
            adjusted = true;
        }
        if (this.config.onSelect) {
            const result = await this.config.onSelect(option);
            // undefined = no change
            // null = cancel operation
            if (typeof result === 'object') {
                option = result;
                adjusted = true;
            }
        }
        return adjusted ? option : undefined;
    }

    // This method is used while switching between different tabs,
    // in an attempt to retain as many search filters as possible.
    filterToKnownTypes(items) {
        const knownTypes = new Set([
            ...this.config.serverSideOptions.map((option) => option.name),
            ...this.config.prefixOptions.map((option) => option.__type__),
            ...this.config.suffixOptions.map((option) => option.__type__),
            ...this.config.computedOptionTypes,
        ]);
        return items.filter((item) => knownTypes.has(item.__type__));
    }
}

export default TypeaheadOptions;


================================================
FILE: src/client/Common/TypeaheadSelector.css
================================================
.rbt input:first-child {
    background-color: var(--input-background-color);
    color: var(--input-text-color);
    font-size: var(--font-size);
}

.rbt .rbt-input-multi {
    background-color: var(--input-background-color);
    border: none;
    border-radius: 0;
    padding: 1px 2px;
}

.rbt .rbt-input-multi .rbt-token {
    background-color: var(--input-background-token-color);
    color: var(--input-text-color);
    font-size: var(--font-size);
    margin: 3px 1px 0;
    padding-bottom: 1px;
    padding-top: 1px;
}

.rbt .rbt-input-multi input:first-child {
    margin-bottom: 2px;
    margin-left: 2px;
}


================================================
FILE: src/client/Common/TypeaheadSelector.js
================================================
import 'react-bootstrap-typeahead/css/Typeahead.min.css';
import './TypeaheadSelector.css';

import PropTypes from 'prop-types';
import React from 'react';
import Button from 'react-bootstrap/Button';
import { AsyncTypeahead } from 'react-bootstrap-typeahead';
import { MdClose } from 'react-icons/md';

import TypeaheadOptions from './TypeaheadOptions';

class TypeaheadSelector extends React.Component {
    constructor(props) {
        super(props);
        this.state = { isLoading: false, text: '', options: [] };
        this.ref = React.createRef();
    }

    onInputChange(text) {
        this.setState({ text });
        this.onSearch(text);
    }

    onSearch(query) {
        this.setState({ isLoading: true });
        let existingItems = [];
        if (this.props.multiple) {
            existingItems = this.props.value;
        } else {
            existingItems = (this.props.value ? [this.props.value] : []);
        }
        this.props.options
            .search(query, existingItems)
            .then((options) => this.setState({ isLoading: false, options }, this.forceUpdate));
    }

    async onChange(selected) {
        if (selected.length) {
            const index = selected.length - 1;
            const result = await this.props.options.select(selected[index]);
            if (result) {
                selected[index] = result;
            } else if (result === null) {
                return;
            }
        }
        if (this.props.multiple) {
            this.props.onChange(selected);
        } else {
            this.props.onChange(selected[0] || null);
        }
    }

    focus() {
        this.ref.current.focus();
    }

    renderDeleteButton() {
        if (this.props.disabled || !this.props.value) {
            return null;
        }
        return (
            <Button
                onClick={() => this.props.onChange(null)}
                title="Cancel"
            >
                <MdClose style={{ fill: 'white !important' }} />
            </Button>
        );
    }

    render() {
        const commonProps = {
            ...this.state,
            id: this.props.id,
            labelKey: 'name',
            minLength: 0,
            onFocus: () => this.onSearch(this.state.text),
            onSearch: (query) => this.onSearch(query),
            placeholder: this.props.placeholder,
            onInputChange: (text) => this.onInputChange(text),
            onChange: (selected) => this.onChange(selected),
            filterBy: (option, props) => true,
            renderMenuItemChildren: (option) => <div>{option.name}</div>,
            ref: this.ref,
        };
        if (this.props.multiple) {
            return (
                <AsyncTypeahead
                    {...commonProps}
                    multiple
                    disabled={this.props.disabled}
                    selected={this.props.value}
                />
            );
        }
        return (
            <>
                <AsyncTypeahead
                    {...commonProps}
                    disabled={this.props.disabled || this.props.value}
                    selected={this.props.value ? [this.props.value] : []}
                />
                {this.renderDeleteButton()}
            </>
        );
    }
}

TypeaheadSelector.propTypes = {
    id: PropTypes.string.isRequired,
    multiple: PropTypes.bool,
    options: PropTypes.instanceOf(TypeaheadOptions).isRequired,

    value: PropTypes.oneOfType([
        // eslint-disable-next-line react/no-typos
        PropTypes.Custom.Item,
        PropTypes.arrayOf(PropTypes.Custom.Item),
    ]),
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,

    placeholder: PropTypes.string,
};

TypeaheadSelector.defaultProps = {
    multiple: false,
};

TypeaheadSelector.getStringItem = (value, index = -1) => ({
    __type__: 'string',
    __id__: index + 1,
    name: value,
});

TypeaheadSelector.getStringListItems = (values) => {
    if (!values) return [];
    return values.map(TypeaheadSelector.getStringItem);
};

TypeaheadSelector.getStringListTypeaheadOptions = (fetcher) => new TypeaheadOptions({
    serverSideOptions: [],
    getComputedOptions: async (query) => {
        // Maybe skip fetching results if query is empty?
        let options = [];
        if (fetcher) {
            options = await fetcher(query);
        }
        options = TypeaheadSelector.getStringListItems(options);
        options.push(TypeaheadSelector.getStringItem(query));
        return options;
    },
});

export default TypeaheadSelector;


================================================
FILE: src/client/Common/URLManager.js
================================================
import assert from 'assert';
import queryString from 'query-string';

let onChange;
let pushState;

const options = { arrayFormat: 'bracket' };

class URLManager {
    static init(callback) {
        assert(!onChange, 'URLManager already initialized');
        onChange = callback;
        pushState = window.history.pushState;
        window.history.pushState = (...args) => {
            pushState.apply(window.history, args);
            onChange();
        };
        return () => {
            window.history.pushState = pushState;
            pushState = null;
            onChange = null;
        };
    }

    static get() {
        return queryString.parse(window.location.search, options);
    }

    static getLink(params) {
        return `?${queryString.stringify(params, options).replace(/%20/g, '+')}`;
    }

    static update(link) {
        window.history.pushState({}, '', link);
    }
}

export default URLManager;


================================================
FILE: src/client/Common/Utils.js
================================================
export function suppressUnlessShiftKey(event) {
    if (!event.shiftKey) {
        event.preventDefault();
    }
}

// https://davidwalsh.name/javascript-debounce-function
export function debounce(func, wait, immediate) {
    let timeout;
    return function inner(...args) {
        const context = this;
        const later = () => {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        const callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
}

export const KeyCodes = {
    DELETE: 8,
    ENTER: 13,
    ESCAPE: 27,
    SPACE: 32,
    UP_ARROW: 38,
    DOWN_ARROW: 40,
};


================================================
FILE: src/client/Common/index.js
================================================
export { default as AsyncSelector } from './AsyncSelector';
export { default as BulletList } from './BulletList';
export { default as Coordinator } from './Coordinator';
export { default as DataLoader } from './DataLoader';
export { default as DateContext } from './DateContext';
export { default as DatePicker } from './DatePicker';
export { default as DateRangePicker } from './DateRangePicker';
export { default as Dropdown } from './Dropdown';
export { default as EditorModal } from './EditorModal';
export { default as EnumSelectorSection } from './EnumSelectorSection';
export { default as ErrorModal } from './ErrorModal';
export { default as Highlightable } from './Highlightable';
export { default as Icon } from './Icon';
export { default as InfoModal } from './InfoModal';
export { default as InputLine } from './InputLine';
export { default as LeftRight } from './LeftRight';
export { default as Link } from './Link';
export { default as ModalStack } from './ModalStack';
export { default as ScrollableSection } from './ScrollableSection';
export { default as Selector } from './Selector';
export { default as SettingsContext } from './SettingsContext';
export { default as SidebarSection } from './SidebarSection';
export { default as SortableList } from './SortableList';
export { default as TextInput } from './TextInput';
export { default as TextEditor } from './TextEditor';
export { default as TooltipElement } from './TooltipElement';
export { default as TypeaheadInput } from './TypeaheadInput';
export { default as TypeaheadOptions } from './TypeaheadOptions';
export { default as TypeaheadSelector } from './TypeaheadSelector';
export { default as URLManager } from './URLManager';

export * from './Plugins';
export * from './StandardIcons';
export * from './Utils';


================================================
FILE: src/client/Graphs/GraphLineChart.js
================================================
import React from 'react';
import {
    CartesianGrid, Legend, Line, LineChart, ResponsiveContainer,
    Tooltip, XAxis, YAxis,
} from 'recharts';

import PropTypes from '../prop-types';

function GraphLineChart(props) {
    const lineElements = props.lines.map((lineItem) => (
        <Line
            key={lineItem.name}
            name={lineItem.name}
            dataKey={lineItem.dataKey}
            type="monotone"
            stroke={lineItem.color || '#00bc8c'}
            connectNulls
            isAnimationActive={false}
        />
    ));
    return (
        <ResponsiveContainer width="100%" height={400}>
            <LineChart
                data={props.samples}
                margin={{
                    top: 20, right: 40, bottom: 50, left: 10,
                }}
            >
                <CartesianGrid strokeDasharray="3 3" />
                <XAxis dataKey="label" angle={-90} dx={-6} dy={40} />
                <YAxis domain={['dataMin', 'dataMax']} scale="linear" />
                <Legend />
                {props.tooltip ? <Tooltip content={props.tooltip} /> : null}
                {lineElements}
            </LineChart>
        </ResponsiveContainer>
    );
}

const LineItem = PropTypes.shape({
    name: PropTypes.string.isRequired,
    dataKey: PropTypes.string.isRequired,
});

GraphLineChart.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    samples: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired,
    lines: PropTypes.arrayOf(LineItem.isRequired).isRequired,
    tooltip: PropTypes.func,
};

export default GraphLineChart;


================================================
FILE: src/client/Graphs/GraphSection.css
================================================
.graph-tooltip {
    background-color: var(--component-color);
    padding: 4px;
    border-radius: 4px;
    white-space: pre-wrap;
}


================================================
FILE: src/client/Graphs/GraphSection.js
================================================
import './GraphSection.css';

import deepEqual from 'deep-equal';
import React from 'react';

import { DataLoader } from '../Common';
import PropTypes from '../prop-types';
import GraphLineChart from './GraphLineChart';
import { getGraphData } from './GraphSectionData';
import GraphSectionOptions, { Granularity } from './GraphSectionOptions';
import { NormalTooltip } from './GraphTooltip';

class GraphSection extends React.Component {
    static getTypeaheadOptions() {
        return GraphSectionOptions.get();
    }

    static getDerivedStateFromProps(props, state) {
        const result = GraphSectionOptions.extractData(props.search);
        result.where.date = props.dateRange || undefined;
        const newGranularity = result.extra.granularity || Granularity.WEEK;
        if (!deepEqual(state.where, result.where)) {
            state.reload = true;
        }
        state.where = result.where;
        state.hasAnyFilters = props.search.length > 0;
        if (state.granularity !== newGranularity && state.logEvents) {
            state.graphData = getGraphData(
                state.where.logStructure,
                state.logEvents,
                props.dateRange,
                newGranularity,
            );
        }
        state.granularity = newGranularity;
        return state;
    }

    constructor(props) {
        super(props);
        this.state = { graphData: null };
    }

    componentDidMount() {
        this.dataLoader = new DataLoader({
            getInput: () => {
                const { hasAnyFilters, where } = this.state;
                if (!hasAnyFilters) {
                    return null;
                }
                return {
                    name: 'log-event-list',
                    args: {
                        where: {
                            ...where,
                            date: this.props.dateRange || undefined,
                        },
                    },
                };
            },
            onData: (logEvents) => {
                const { dateRange } = this.props;
                const { where, granularity } = this.state;
                const graphData = getGraphData(
                    where.logStructure,
                    logEvents,
                    dateRange,
                    granularity,
                );
                this.setState({ logEvents, graphData });
            },
        });
    }

    componentDidUpdate(prevProps) {
        if (this.state.reload) {
            // eslint-disable-next-line react/no-did-update-set-state
            this.setState({ reload: false });
            this.dataLoader.reload();
        }
    }

    componentWillUnmount() {
        this.dataLoader.stop();
    }

    render() {
        const { hasAnyFilters, graphData } = this.state;
        if (!hasAnyFilters) {
            return 'Please add some filters!';
        } if (!graphData) {
            return 'Loading ...';
        } if (graphData.isEmpty) {
            return 'No data!';
        }
        return graphData.lines.map((line) => {
            const lines = [{
                name: line.name,
                dataKey: line.valueKey,
            }];
            return (
                <GraphLineChart
                    key={line.name}
                    samples={graphData.samples}
                    lines={lines}
                    tooltip={NormalTooltip}
                />
            );
        });
    }
}

GraphSection.propTypes = {
    dateRange: PropTypes.Custom.DateRange,
    search: PropTypes.arrayOf(PropTypes.Custom.Item.isRequired).isRequired,
};

export default GraphSection;


================================================
FILE: src/client/Graphs/GraphSectionData.js
================================================
import { addDays, compareAsc } from 'date-fns';

import { LogKey } from '../../common/data_types';
import DateUtils from '../../common/DateUtils';
import RichTextUtils from '../../common/RichTextUtils';
import { Granularity } from './GraphSectionOptions';

function getLogKeyValues(keyIndex, valueParser, logEvents) {
    return logEvents.map((logEvent) => {
        const logKey = logEvent.logStructure.eventKeys[keyIndex];
        if (logKey.value === null) {
            return null;
        }
        return valueParser(logKey.value);
    }).filter((value) => value !== null);
}

function getAverageValue(values) {
    if (values.length === 0) {
        return null;
    } if (values.length === 1) {
        return values[0];
    }
    // logEvents[0].logStructure.eventKeys[keyIndex].aggregationType?
    return values.reduce((result, value) => (result + value), 0) / values.length;
}

function getLines(logStructure, logEvent) {
    const lines = [];
    lines.push({
        valueKey: 'event_count',
        valuesKey: 'event_count_values',
        name: 'Event Count',
        getValues: (logEvents) => [logEvents.length],
    });
    if (logStructure) {
        logEvent.logStructure.eventKeys.forEach((logKey, index) => {
            let valueParser;
            if (logKey.type === LogKey.Type.INTEGER) {
                valueParser = parseInt;
            } else if (logKey.type === LogKey.Type.NUMBER) {
                valueParser = parseFloat;
            } else if (logKey.type === LogKey.Type.TIME) {
                valueParser = (value) => parseInt(value.replace(':', ''), 10);
            } else {
                return;
            }
            lines.push({
                valueKey: `key_${logKey.__id__}_value`,
                valuesKey: `key_${logKey.__id__}_values`,
                name: `Key: ${logKey.name}`,
                getValues: getLogKeyValues.bind(null, index, valueParser),
            });
        });
    }
    // TODO: Add support for custom graphs?
    return lines;
}

function getTimeSeries(logEvents, lines, dateRange, granularity) {
    if (logEvents.length === 0) {
        return [];
    }
    const dateLabelToLogEvents = {};
    logEvents.forEach((item) => {
        if (!item.date) {
            return;
        }
        if (!(item.date in dateLabelToLogEvents)) {
            dateLabelToLogEvents[item.date] = [];
        }
        dateLabelToLogEvents[item.date].push(item);
    });
    let startDate;
    let endDate;
    if (dateRange) {
        startDate = DateUtils.getDate(dateRange.startDate);
        endDate = DateUtils.getDate(dateRange.endDate);
    } else {
        const dateLabels = Object.keys(dateLabelToLogEvents).sort();
        startDate = DateUtils.getDate(dateLabels[0]);
        endDate = DateUtils.getDate(dateLabels[dateLabels.length - 1]);
    }
    const samples = [];
    for (
        let currentDate = startDate;
        compareAsc(currentDate, endDate) <= 0;
    ) {
        const currentLogEvents = [];
        let label = null;
        // eslint-disable-next-line no-constant-condition
        while (true) {
            const nextLabel = Granularity[granularity].getLabel(currentDate);
            if (label === null) {
                label = nextLabel;
            } else if (label === nextLabel) {
                // nothing changes
            } else {
                break; // move to next group
            }
            const dateLabel = DateUtils.getLabel(currentDate);
            const nextLogEvents = dateLabelToLogEvents[dateLabel] || [];
            currentLogEvents.push(...nextLogEvents);
            currentDate = addDays(currentDate, 1);
        }
        const sample = { label };
        lines.forEach((line) => {
            const values = line.getValues(currentLogEvents);
            sample[line.valuesKey] = values;
            sample[line.valueKey] = getAverageValue(values);
        });
        sample.logEventTitles = currentLogEvents.map(
            (logEvent) => `${logEvent.date}: ${RichTextUtils.extractPlainText(logEvent.title)}`,
        );
        samples.push(sample);
    }
    return samples;
}

// eslint-disable-next-line import/prefer-default-export
export function getGraphData(logStructure, logEvents, dateRange, granularity) {
    if (!logEvents || !logEvents.length) return { isEmpty: true };
    try {
        const lines = getLines(logStructure, logEvents[0]);
        const samples = getTimeSeries(logEvents, lines, dateRange, granularity);
        return { lines, samples };
    } catch (error) {
        // eslint-disable-next-line no-console
        console.error(error);
        throw error;
    }
}


================================================
FILE: src/client/Graphs/GraphSectionOptions.js
================================================
import {
    getDay, getMonth, getYear, subDays,
} from 'date-fns';

import { Enum } from '../../common/data_types';
import DateUtils from '../../common/DateUtils';
import { LogEventOptions } from '../LogEvent';

const Granularity = Enum([
    {
        label: 'Day',
        value: 'day',
        getLabel: (date) => DateUtils.getLabel(date),
    },
    {
        label: 'Week',
        value: 'week',
        getLabel: (date) => {
            const dayOfWeek = getDay(date);
            const startDateOfWeek = subDays(date, dayOfWeek);
            return DateUtils.getLabel(startDateOfWeek);
        },
    },
    {
        label: 'Month',
        value: 'month',
        getLabel: (date) => {
            let month = (getMonth(date) + 1).toString();
            month = (month.length === 1 ? '0' : '') + month;
            return `${getYear(date)}-${month}`;
        },
    },
]);

const GRANULARITY_TYPE = 'graph-granularity';
const GRANULARITY_PREFIX = 'Granularity: ';
const GRANULARITY_OPTIONS = Granularity.Options.map((option, index) => ({
    __type__: GRANULARITY_TYPE,
    __id__: -index - 1,
    name: GRANULARITY_PREFIX + option.label,
}));

const GRANULARITY_MOCK_OPTION = {
    __type__: GRANULARITY_TYPE,
    apply: (item, _where, extra) => {
        extra.granularity = item.name.substr(GRANULARITY_PREFIX.length).toLowerCase();
    },
};

class GraphSectionOptions {
    static get() {
        return LogEventOptions.get(GRANULARITY_OPTIONS);
    }

    static extractData(items) {
        const result = LogEventOptions.extractData(
            items,
            LogEventOptions.getTypeToActionMap([GRANULARITY_MOCK_OPTION]),
        );
        return result;
    }
}

export { Granularity };
export default GraphSectionOptions;


================================================
FILE: src/client/Graphs/GraphTooltip.js
================================================
import PropTypes from 'prop-types';
import React from 'react';

function NormalTooltip({ active, label, payload }) {
    if (active && payload && payload.length) {
        const item = payload[0];
        const output = [];
        output.push(`Group: ${label}`);
        output.push(`${item.name}: ${item.payload[item.dataKey]}`);
        const { logEventTitles } = item.payload;
        if (logEventTitles.length) {
            output.push('', ...logEventTitles);
        }
        return (
            <div className="graph-tooltip">
                {output.map((line) => `${line}\n`).join('')}
            </div>
        );
    }
    return null;
}

NormalTooltip.propTypes = {
    active: PropTypes.bool,
    label: PropTypes.string,
    // eslint-disable-next-line react/forbid-prop-types
    payload: PropTypes.any,
};

// eslint-disable-next-line import/prefer-default-export
export { NormalTooltip };


================================================
FILE: src/client/Graphs/index.js
================================================
export { default as GraphSection } from './GraphSection';
export { default as GraphLineChart } from './GraphLineChart';
export { Granularity } from './GraphSectionOptions';
export { getGraphData } from './GraphSectionData';


================================================
FILE: src/client/LogEvent/LogEventAdder.js
================================================
import PropTypes from 'prop-types';
import React from 'react';

import { isRealItem, LogEvent } from '../../common/data_types';
import {
    Coordinator, KeyCodes, TextEditor, TypeaheadOptions,
} from '../Common';
import LogEventEditor from './LogEventEditor';

class LogEventAdder extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            logEvent: LogEvent.createVirtual(this.props.where),
        };
    }

    onEditLogEvent(logEvent) {
        this.setState({ logEvent: LogEvent.createVirtual(this.props.where) });
        Coordinator.invoke('modal-editor', {
            dataType: 'log-event',
            EditorComponent: LogEventEditor,
            valueKey: 'logEvent',
            value: logEvent,
        });
    }

    onSaveLogEvent(logEvent) {
        if (logEvent.title) {
            window.api.send('log-event-upsert', logEvent)
                .then((_newLogEvent) => {
                    // The new LogEvent would have been added to list, so we can reset this.
                    this.setState({ logEvent: LogEvent.createVirtual(this.props.where) });
                });
        } else {
            this.onEditLogEvent(logEvent);
        }
    }

    async onSelect(option) {
        if (option.__type__ === 'log-structure') {
            const logStructure = await window.api.send('log-structure-load', option);
            const updatedLogEvent = LogEvent.createVirtual({
                ...this.props.where,
                logStructure,
            });
            LogEvent.trigger(updatedLogEvent);
            if (logStructure.eventNeedsEdit) {
                this.onEditLogEvent(updatedLogEvent);
            } else {
                this.onSaveLogEvent(updatedLogEvent);
            }
        }
    }

    render() {
        const { logEvent } = this.state;
        return (
            <TextEditor
                isSingleLine
                focusOnLoad
                unstyled
                minWidth
                placeholder="Add Event ..."
                value={logEvent.title}
                options={new TypeaheadOptions({
                    serverSideOptions: [{ name: 'log-structure' }, { name: 'log-topic' }],
                    onSelect: (option) => this.onSelect(option),
                })}
                disabled={isRealItem(logEvent.logStructure)}
                onChange={(value) => {
                    const updatedLogEvent = { ...logEvent };
                    updatedLogEvent.title = value;
                    LogEvent.trigger(updatedLogEvent);
                    this.setState({ logEvent: updatedLogEvent });
                }}
                onSpecialKeys={(event) => {
                    if (event.keyCode === KeyCodes.ENTER) {
                        this.onSaveLogEvent(logEvent);
                    }
                }}
                {...this.props}
            />
        );
    }
}

LogEventAdder.propTypes = {
    // eslint-disable-next-line react/forbid-prop-types
    where: PropTypes.object,
};

LogEventAdder.defaultProps = {
    where: {},
};

export default LogEventAdder;


================================================
FILE: src/client/LogEvent/LogEventDetailsHeader.js
================================================
import PropTypes from 'prop-types';
import React from 'react';

import {
    InputLine, TextEditor,
} from '../Common';

class LogEventDetailsHeader extends React.Component {
    renderTitle() {
        const { logEvent } = this.props;
        return (
            <InputLine styled className="px-2">
                <TextEditor
                    isSingleLine
                    unstyled
                    disabled
                    value={logEvent.title}
                />
            </InputLine>
        );
    }

    render() {
        return this.renderTitle();
    }
}

LogEventDetailsHeader.propTypes = {
    logEvent: PropTypes.Custom.LogEvent.isRequired,
};

export default LogEventDetailsHeader;


================================================
FILE: src/client/LogEvent/LogEventEditor.js
================================================
import PropTypes from 'prop-types';
import React from 'react';
import InputGroup from 'react-bootstrap/InputGroup';

import { LogEvent } from '../../common/data_types';
import {
    DatePicker, Selector, TextEditor, TypeaheadOptions, TypeaheadSelector,
} from '../Common';
import { LogValueListEditor } from '../LogKey';

const { LogLevel } = LogEvent;

class LogEventEditor extends React.Component {
    constructor(props) {
        super(props);
        this.titleRef = React.createRef();
        this.detailsRef = React.createRef();
        this.valueListRef = React.createRef();
    }

    componentDidMount() {
        const { logEvent } = this.props;
        if (logEvent.logStructure) {
            if (logEvent.logStructure.eventKeys.length) {
                this.valueListRef.current.focus();
            } else {
                this.detailsRef.current.focus();
            }
        } else {
            this.titleRef.current.focus();
        }
    }

    updateLogEvent(methodOrName, maybeValue) {
        const updatedLogEvent = { ...this.props.logEvent };
        if (typeof methodOrName === 'function') {
            methodOrName(updatedLogEvent);
        } else {
            updatedLogEvent[methodOrName] = maybeValue;
        }
        LogEvent.trigger(updatedLogEvent);
        this.props.onChange(updatedLogEvent);
    }

    renderDate() {
        return (
            <InputGroup className="my-1">
                <InputGroup.Text>
                    {this.props.logEvent.isComplete ? 'Date' : 'Deadline Date'}
                </InputGroup.Text>
                <DatePicker
                    date={this.props.logEvent.date}
                    disabled={this.props.disabled}
                    onChange={(date) => this.updateLogEvent('date', date)}
                />
            </InputGroup>
        );
    }

    renderIsComplete() {
        return (
            <InputGroup className="my-1">
                <InputGroup.Text>
                    Complete?
                </InputGroup.Text>
                <Selector.Binary
                    value={this.props.logEvent.isComplete}
                    disabled={this.props.disabled}
                    onChange={(isComplete) => this.updateLogEvent('isComplete', isComplete)}
                />
            </InputGroup>
        );
    }

    renderTitle() {
        return (
            <InputGroup className="my-1">
                <InputGroup.Text>
                    Title
                </InputGroup.Text>
                <TextEditor
                    isSingleLine
                    value={this.props.logEvent.title}
                    options={new TypeaheadOptions({
                        serverSideOptions: [{ name: 'log-structure' }, { name: 'log-topic' }],
                        onSelect: async (option) => {
                            if (option.__type__ === 'log-structure') {
                                const logStructure = await window.api.send('log-structure-load', option);
                                this.updateLogEvent('logStructure', logStructure);
                            }
                        },
                    })}
                    disabled={this.props.disabled || !!this.props.logEvent.logStructure}
                    onChange={(title) => this.updateLogEvent('title', title)}
                    onSpecialKeys={this.props.onSpecialKeys}
                    ref={this.titleRef}
                />
            </InputGroup>
        );
    }

    renderDetails() {
        const { logEvent } = this.props;
        const eventAllowDetails = logEvent.logStructure
            ? logEvent.logStructure.eventAllowDetails
            : true;
        return (
            <InputGroup className="my-1">
                <InputGroup.Text>
                    Details
                </InputGroup.Text>
                <TextEditor
                    value={logEvent.details}
                    options={TypeaheadOptions.getFromTypes(['log-topic'])}
                    disabled={this.props.disabled || !eventAllowDetails}
                    onChange={(details) => this.updateLogEvent('details', details)}
                    ref={this.detailsRef}
                />
            </InputGroup>
        );
    }

    renderLogLevel() {
        return (
            <InputGroup className="my-1">
                <InputGroup.Text>
                    Log Level
                </InputGroup.Text>
                <Selector
                    options={LogLevel.Options}
                    value={LogLevel.getValue(this.props.logEvent.logLevel)}
                    disabled={this.props.disabled}
                    onChange={(value) => this.updateLogEvent('logLevel', LogLevel.getIndex(value))}
                />
            </InputGroup>
        );
    }

    renderStructureSelector() {
        return (
            <InputGroup className="my-1">
                <InputGroup.Text>
                    Structure
                </InputGroup.Text>
                <TypeaheadSelector
                    id="log-event-editor-structure"
                    options={new TypeaheadOptions({
                        serverSideOptions: [{ name: 'log-structure' }],
                        onSelect: (option) => window.api.send('log-structure-load', option),
                    })}
                    value={this.props.logEvent.logStructure}
                    disabled={this.props.disabled}
                    onChange={(logStructure) => this.updateLogEvent((updatedLogEvent) => {
                        updatedLogEvent.logStructure = logStructure;
                        if (logStructure) {
                            LogEvent.addDefaultStructureValues(updatedLogEvent);
                        } else {
                            updatedLogEvent.title = null;
                        }
                    })}
                    allowDelete
                />
            </InputGroup>
        );
    }

    renderStructureValues() {
        const { logEvent } = this.props;
        if (!logEvent.logStructure || logEvent.logStructure.eventKeys.length === 0) {
            return null;
        }
        return (
            <LogValueListEditor
                source={logEvent.logStructure}
                logKeys={logEvent.logStructure.eventKeys}
                disabled={this.props.disabled}
                onChange={(updatedLogKeys) => this.updateLogEvent((updatedLogEvent) => {
                    updatedLogEvent.logStructure.eventKeys = updatedLogKeys;
                })}
                ref={this.valueListRef}
            />
        );
    }

    render() {
        return (
            <div>
                <div className="my-3">
                    {this.renderDate()}
                    {this.renderIsComplete()}
                </div>
                <div className="my-3">
                    {this.renderTitle()}
                    {this.renderDetails()}
                </div>
                {this.renderLogLevel()}
                <div className="my-3">
                    {this.renderStructureSelector()}
                    {this.renderStructureValues()}
                </div>
            </div>
        );
    }
}

LogEventEditor.propTypes = {
    logEvent: PropTypes.Custom.LogEvent.isRequired,
    disabled: PropTypes.bool.isRequired,
    onChange: PropTypes.func.isRequired,
    onSpecialKeys: PropTypes.func,
};

export default LogEventEditor;


================================================
FILE: src/client/LogEvent/LogEventList.js
================================================
import PropTypes from 'prop-types';
import React from 'react';

import { BulletList, DetailsIcon, TextEditor } from '../Common';
import LogEventAdder from './LogEventAdder';
import LogEventEditor from './LogEventEditor';

function LogEventViewer(props) {
    const { logEvent } = props;
    let datePrefix;
    if (props.displayDate) {
        datePrefix = (
            <span className="float-left monospace">
                {`${logEvent.date}: `}
            </span>
        );
    }
    const title = (
        <span className="ml-1">
            <TextEditor.SimpleViewer
                unstyled
                disabled
                value={logEvent.title}
            />
        </span>
    );
    let detailsSuffix;
    if (logEvent.details) {
        detailsSuffix = (
            <DetailsIcon
                onClick={props.toggleExpansion}
                isShown
            />
        );
    }
    let logLevelSuffix;
    if (props.displayLogLevel) {
        logLevelSuffix = (
            <span className="float-right ml-1">
                {`L${logEvent.logLevel}`}
            </span>
        );
    }
    return (
        <div>
            {datePrefix}
            {logLevelSuffix}
            {title}
            {detailsSuffix}
        </div>
    );
}

LogEventViewer.propTypes = {
    logEvent: PropTypes.Custom.LogEvent.isRequired,
    displayDate: PropTypes.bool,
    displayLogLevel: PropTypes.bool,
    toggleExpansion: PropTypes.func.isRequired,
};

LogEventViewer.Expanded = function (props) {
    const { logEvent } = props;
    if (!logEvent.details) {
        return null;
    }
    return (
        <TextEditor
            unstyled
            disabled
            value={logEvent.details}
        />
    );
};

LogEventViewer.Expanded.propTypes = {
    logEvent: PropTypes.Custom.LogEvent.isRequired,
};

function LogEventList(props) {
    const { showAdder, ...moreProps } = props;
    return (
        <BulletList
            {...moreProps}
            dataType="log-event"
            valueKey="logEvent"
            allowSubscription
            ViewerComponent={LogEventViewer}
            EditorComponent={LogEventEditor}
            AdderComponent={showAdder ? LogEventAdder : null}
        />
    );
}

LogEventList.propTypes = {
    name: PropTypes.string.isRequired,
    showAdder: PropTypes.bool,
};

LogEventList.Single = function (props) {
    const { logEvent, ...moreProps } = props;
    return (
        <BulletList.Item
            {...moreProps}
            dataType="log-event"
            value={logEvent}
            valueKey="logEvent"
            ViewerComponent={LogEventViewer}
            EditorComponent={LogEventEditor}
        />
    );
};

LogEventList.Single.propTypes = {
    logEvent: PropTypes.Custom.LogEvent.isRequired,
};

export default LogEventList;


================================================
FILE: src/client/LogEvent/LogEventOptions.js
================================================
import assert from 'assert';

import { getVirtualID } from '../../common/data_types';
import { TypeaheadOptions } from '../Common';

const NO_STRUCTURE_ITEM = {
    __type__: 'log-structure',
    __id__: 0,
    name: 'No Structure',
};

const EVENT_TITLE_ITEM_TYPE = 'log-event-title';
const EVENT_TITLE_ITEM_PREFIX = 'Title: ';

class LogEventOptions {
    static get(prefixOptions) {
        prefixOptions = [...prefixOptions, NO_STRUCTURE_ITEM];
        return new TypeaheadOptions({
            serverSideOptions: [
                { name: 'log-topic' },
                { name: 'log-structure' },
            ],
            prefixOptions,
            computedOptionTypes: [EVENT_TITLE_ITEM_TYPE],
            getComputedOptions: async (query) => {
                const options = [];
                if (query) {
                    options.push({
                        __type__: EVENT_TITLE_ITEM_TYPE,
                        __id__: getVirtualID(),
                        name: EVENT_TITLE_ITEM_PREFIX + query,
                    });
                }
                return options;
            },
            onSelect: (option) => {
                if (option && option.getItem) {
                    return option.getItem(option);
                }
                return undefined;
            },
        });
    }

    static getTypeToActionMap(extraOptions) {
        const result = {
            'log-structure': (item, where, extra) => {
                // This also handles NO_STRUCTURE_ITEM.
                assert(!Object.prototype.hasOwnProperty.call(where, 'logStructure'));
                where.logStructure = item.__id__ ? item : null;
                extra.searchView = true;
            },
            'log-topic': (item, where, extra) => {
                if (!where.logTopics) {
                    where.logTopics = [];
                }
                where.logTopics.push(item);
                extra.searchView = true;
            },
            [EVENT_TITLE_ITEM_TYPE]: (item, where, extra) => {
                where.title = item.name.substring(EVENT_TITLE_ITEM_PREFIX.length);
                extra.searchView = true;
            },
        };
        if (extraOptions) {
            extraOptions.forEach((item) => {
                assert(typeof item.apply === 'function', `Missing apply method on ${item}`);
                result[item.__type__] = item.apply;
            });
        }
        return result;
    }

    static extractData(items, typeToActionMap, defaultWhere) {
        const where = { ...defaultWhere };
        const extra = {};
        items.forEach((item) => {
            const action = typeToActionMap[item.__type__];
            if (action) {
                action(item, where, extra);
            } else {
                assert(false, `Unable to process ${JSON.stringify(item)}`);
            }
        });
        return { where, extra };
    }
}

export default LogEventOptions;


================================================
FILE: src/client/LogEvent/LogEventSearch.js
================================================
import assert from 'assert';
import { addDays, eachDayOfInterval, getDay } from 'date-fns';
import React from 'react';

import { getVirtualID, LogEvent } from '../../common/data_types';
import DateUtils from '../../common/DateUtils';
import { Coordinator, DateContext, SettingsContext } from '../Common';
import PropTypes from '../prop-types';
import LogEventEditor from './LogEventEditor';
import LogEventList from './LogEventList';
import LogEventOptions from './LogEventOptions';

// Extra Filters for Events

const INCOMPLETE_ITEM = {
    __type__: 'incomplete',
    __id__: getVirtualID(),
    name: 'Incomplete Events',
    apply: (_item, where, _extra) => {
        where.isComplete = false;
    },
};

const LOG_LEVEL_MINOR_ITEM = {
    __type__: 'log-event-level',
    __id__: getVirtualID(),
    name: 'Log Level: Minor+',
};
const LOG_LEVEL_MAJOR_ITEM = {
    __type__: 'log-event-level',
    __id__: getVirtualID(),
    name: 'Log Level: Major+',
};
const LOG_LEVEL_MOCK_ITEM = {
    __type__: 'log-event-level',
    apply: (item, where, extra) => {
        if (item.__id__ === LOG_LEVEL_MINOR_ITEM.__id__) {
            delete where.logLevel; // [1, 2, 3]
            extra.allowReordering = true;
        } else if (item.__id__ === LOG_LEVEL_MAJOR_ITEM.__id__) {
            where.logLevel = [3];
            extra.searchView = true;
        }
    },
};

const DEFAULT_WHERE = {
    isComplete: true,
    logLevel: [2, 3],
};

function getDayOfTheWeek(label) {
    return DateUtils.DaysOfTheWeek[getDay(DateUtils.getDate(label))];
}

class LogEventSearch extends React.Component {
    static getTypeaheadOptions() {
        return LogEventOptions.get([
            INCOMPLETE_ITEM,
            LOG_LEVEL_MINOR_ITEM,
            LOG_LEVEL_MAJOR_ITEM,
        ]);
    }

    static getDerivedStateFromProps(props, _state) {
        return LogEventOptions.extractData(
            props.search,
            LogEventOptions.getTypeToActionMap([
                INCOMPLETE_ITEM,
                LOG_LEVEL_MOCK_ITEM,
            ]),
            DEFAULT_WHERE,
        );
    }

    constructor(props) {
        super(props);
        this.state = {};
    }

    componentDidMount() {
        this.deregisterCallbacks = [
            Coordinator.subscribe('log-event-created', (logEvent) => {
                if (logEvent.logLevel === 1 && !this.props.search.length) {
                    Coordinator.invoke('url-update', { search: [LOG_LEVEL_MINOR_ITEM] });
                }
            }),
       
Download .txt
gitextract_nsqvf378/

├── .gitignore
├── .husky/
│   └── pre-commit
├── LICENSE
├── README.md
├── config/
│   ├── babel.config.js
│   ├── demo.glados.json
│   ├── eslint.config.js
│   ├── example.glados.json
│   ├── jest.config.js
│   └── webpack.config.js
├── package.json
└── src/
    ├── README.md
    ├── client/
    │   ├── Application/
    │   │   ├── Application.js
    │   │   ├── BackupSection.js
    │   │   ├── CreditsSection.js
    │   │   ├── DetailsSection.css
    │   │   ├── DetailsSection.js
    │   │   ├── FavoritesSection.js
    │   │   ├── IndexSection.css
    │   │   ├── IndexSection.js
    │   │   ├── TabSection.js
    │   │   ├── URLState.js
    │   │   └── index.js
    │   ├── Bootstrap/
    │   │   ├── InputGroup.css
    │   │   ├── Modal.css
    │   │   ├── Popover.css
    │   │   └── index.js
    │   ├── Common/
    │   │   ├── AddLinkPlugin.js
    │   │   ├── AsyncSelector.js
    │   │   ├── BulletList/
    │   │   │   ├── BulletList.css
    │   │   │   ├── BulletList.js
    │   │   │   ├── BulletListIcon.js
    │   │   │   ├── BulletListItem.js
    │   │   │   ├── BulletListLine.js
    │   │   │   ├── BulletListPager.js
    │   │   │   ├── BulletListTitle.js
    │   │   │   └── index.js
    │   │   ├── ConfirmModal.js
    │   │   ├── Coordinator.js
    │   │   ├── DataLoader.js
    │   │   ├── DateContext.js
    │   │   ├── DatePicker.js
    │   │   ├── DateRangePicker.js
    │   │   ├── Dropdown.css
    │   │   ├── Dropdown.js
    │   │   ├── EditorModal.js
    │   │   ├── EnumSelectorSection.js
    │   │   ├── ErrorModal.js
    │   │   ├── Highlightable.css
    │   │   ├── Highlightable.js
    │   │   ├── Icon.css
    │   │   ├── Icon.js
    │   │   ├── InfoModal.js
    │   │   ├── InputLine.css
    │   │   ├── InputLine.js
    │   │   ├── LeftRight.js
    │   │   ├── Link.js
    │   │   ├── ModalStack.js
    │   │   ├── Plugins.js
    │   │   ├── PopoverElement.js
    │   │   ├── ScrollableSection.css
    │   │   ├── ScrollableSection.js
    │   │   ├── Selector.js
    │   │   ├── SettingsContext.js
    │   │   ├── SidebarSection.css
    │   │   ├── SidebarSection.js
    │   │   ├── SortableList.css
    │   │   ├── SortableList.js
    │   │   ├── StandardIcons.js
    │   │   ├── TextEditor.css
    │   │   ├── TextEditor.js
    │   │   ├── TextInput.js
    │   │   ├── TooltipElement.js
    │   │   ├── TypeaheadInput.js
    │   │   ├── TypeaheadOptions.js
    │   │   ├── TypeaheadSelector.css
    │   │   ├── TypeaheadSelector.js
    │   │   ├── URLManager.js
    │   │   ├── Utils.js
    │   │   └── index.js
    │   ├── Graphs/
    │   │   ├── GraphLineChart.js
    │   │   ├── GraphSection.css
    │   │   ├── GraphSection.js
    │   │   ├── GraphSectionData.js
    │   │   ├── GraphSectionOptions.js
    │   │   ├── GraphTooltip.js
    │   │   └── index.js
    │   ├── LogEvent/
    │   │   ├── LogEventAdder.js
    │   │   ├── LogEventDetailsHeader.js
    │   │   ├── LogEventEditor.js
    │   │   ├── LogEventList.js
    │   │   ├── LogEventOptions.js
    │   │   ├── LogEventSearch.js
    │   │   └── index.js
    │   ├── LogKey/
    │   │   ├── LogKeyEditor.js
    │   │   ├── LogKeyListEditor.js
    │   │   ├── LogValueEditor.js
    │   │   ├── LogValueListEditor.js
    │   │   └── index.js
    │   ├── LogStructure/
    │   │   ├── LogStructureDetailsHeader.js
    │   │   ├── LogStructureEditor.js
    │   │   ├── LogStructureFrequencyEditor.js
    │   │   ├── LogStructureGroupEditor.js
    │   │   ├── LogStructureGroupList.js
    │   │   ├── LogStructureList.js
    │   │   ├── LogStructureOptions.js
    │   │   ├── LogStructureSearch.js
    │   │   └── index.js
    │   ├── LogTopic/
    │   │   ├── LogTopicDetailsHeader.js
    │   │   ├── LogTopicEditor.js
    │   │   ├── LogTopicList.js
    │   │   ├── LogTopicOptions.js
    │   │   ├── LogTopicSearch.js
    │   │   └── index.js
    │   ├── Reminders/
    │   │   ├── ReminderItem.js
    │   │   ├── ReminderList.js
    │   │   ├── ReminderSidebar.js
    │   │   └── index.js
    │   ├── Settings/
    │   │   ├── SettingsEditor.js
    │   │   ├── SettingsModal.js
    │   │   ├── SettingsSection.js
    │   │   └── index.js
    │   ├── __tests__/
    │   │   └── Colors.test.js
    │   ├── index.css
    │   ├── index.html
    │   ├── index.js
    │   └── prop-types.js
    ├── common/
    │   ├── AsyncUtils.js
    │   ├── DateUtils.js
    │   ├── RichTextUtils.js
    │   ├── SocketRPC.js
    │   ├── __tests__/
    │   │   └── RichTextUtils.test.js
    │   ├── data_types/
    │   │   ├── LogEvent.js
    │   │   ├── LogKey.js
    │   │   ├── LogStructure.js
    │   │   ├── LogStructureFrequency.js
    │   │   ├── LogStructureGroup.js
    │   │   ├── LogTopic.js
    │   │   ├── __tests__/
    │   │   │   └── LogStructureFrequency.test.js
    │   │   ├── api.js
    │   │   ├── base.js
    │   │   ├── enum.js
    │   │   ├── index.js
    │   │   ├── utils.js
    │   │   └── validation.js
    │   └── polyfill.js
    ├── demo/
    │   ├── components/
    │   │   ├── Application.js
    │   │   ├── BaseWrapper.js
    │   │   ├── BulletList.js
    │   │   ├── DetailsSection.js
    │   │   ├── IndexSection.js
    │   │   ├── Inputs.js
    │   │   ├── ModalDialog.js
    │   │   ├── ReminderItem.js
    │   │   ├── SidebarSection.js
    │   │   └── index.js
    │   ├── index.js
    │   ├── lessons/
    │   │   ├── 001-events.js
    │   │   ├── 002-topics.js
    │   │   ├── 003-structures.js
    │   │   ├── 004-reminders.js
    │   │   └── 005-graphs.js
    │   ├── lessons.js
    │   └── process.js
    ├── plugins/
    │   ├── README.md
    │   └── kaustubh/
    │       ├── custom.actions.js
    │       ├── long_term_goals/
    │       │   ├── LongTermGoalGraph.js
    │       │   ├── LongTermGoalsSettings.js
    │       │   └── client.js
    │       ├── more_event_lists/
    │       │   ├── MoreEventListsSettings.js
    │       │   └── client.js
    │       ├── time_sections/
    │       │   ├── TimeSection.js
    │       │   ├── TimeSectionSettings.js
    │       │   └── client.js
    │       ├── topic_reminder_sections/
    │       │   ├── TopicRemindersSection.js
    │       │   ├── TopicRemindersSectionSettings.js
    │       │   ├── actions.js
    │       │   └── client.js
    │       └── topic_sections/
    │           ├── TopicSection.js
    │           ├── TopicSectionSettings.js
    │           └── client.js
    └── server/
        ├── __tests__/
        │   └── Config.test.js
        ├── actions/
        │   ├── __tests__/
        │   │   ├── Backup.test.js
        │   │   ├── Database.test.js
        │   │   ├── LogEvent.test.js
        │   │   ├── LogStructure.test.js
        │   │   ├── LogTopic.test.js
        │   │   ├── Reminders.test.js
        │   │   └── TestUtils.js
        │   ├── backup.js
        │   ├── data_types.js
        │   ├── database.js
        │   ├── reminders.js
        │   ├── settings.js
        │   └── suggestions.js
        ├── actions.js
        ├── database.js
        ├── index.js
        └── models.js
Download .txt
SYMBOL INDEX (841 symbols across 139 files)

FILE: config/webpack.config.js
  function fromProjectRoot (line 8) | function fromProjectRoot(relativePath) {
  function getJSModuleRule (line 12) | function getJSModuleRule() {
  function getStats (line 27) | function getStats() {
  function getClientSideBundle (line 40) | function getClientSideBundle(entryPoint, outputFileName) {
  function getServerSideBundle (line 88) | function getServerSideBundle(entryPoint, outputFileName) {

FILE: src/client/Application/Application.js
  class Applicaton (line 52) | class Applicaton extends React.Component {
    method constructor (line 53) | constructor(props) {
    method componentDidMount (line 59) | componentDidMount() {
    method componentDidUpdate (line 76) | componentDidUpdate() {
    method componentWillUnmount (line 80) | componentWillUnmount() {
    method renderLeftSidebar (line 85) | renderLeftSidebar() {
    method renderCenterSection (line 105) | renderCenterSection() {
    method renderRightSidebar (line 166) | renderRightSidebar() {
    method renderRightSidebarWidgets (line 222) | renderRightSidebarWidgets() {
    method render (line 267) | render() {

FILE: src/client/Application/BackupSection.js
  class BackupSection (line 7) | class BackupSection extends React.Component {
    method onClick (line 8) | static onClick() {
    method constructor (line 16) | constructor(props) {
    method componentDidMount (line 21) | componentDidMount() {
    method componentWillUnmount (line 30) | componentWillUnmount() {
    method render (line 34) | render() {

FILE: src/client/Application/CreditsSection.js
  function CreditsSection (line 5) | function CreditsSection(props) {

FILE: src/client/Application/DetailsSection.js
  constant HEADER_MAPPING (line 22) | const HEADER_MAPPING = {
  class DetailsSection (line 40) | class DetailsSection extends React.Component {
    method constructor (line 41) | constructor(props) {
    method componentDidMount (line 51) | componentDidMount() {
    method componentDidUpdate (line 95) | componentDidUpdate() {
    method componentWillUnmount (line 99) | componentWillUnmount() {
    method onChange (line 103) | onChange(item) {
    method onEditButtonClick (line 111) | onEditButtonClick() {
    method saveNotDebounced (line 122) | saveNotDebounced() {
    method renderPrefixButtons (line 135) | renderPrefixButtons(item) {
    method renderSuffixButtons (line 163) | renderSuffixButtons(item) {
    method renderHeader (line 181) | renderHeader() {
    method renderKeys (line 215) | renderKeys() {
    method renderDetails (line 238) | renderDetails() {
    method render (line 268) | render() {

FILE: src/client/Application/FavoritesSection.js
  class FavoritesSection (line 6) | class FavoritesSection extends React.Component {
    method constructor (line 7) | constructor(props) {
    method componentDidMount (line 12) | componentDidMount() {
    method componentWillUnmount (line 29) | componentWillUnmount() {
    method renderContent (line 33) | renderContent() {
    method render (line 47) | render() {

FILE: src/client/Application/IndexSection.js
  class IndexSection (line 7) | class IndexSection extends React.Component {
    method renderWithTypeahead (line 8) | renderWithTypeahead() {
    method renderSimple (line 44) | renderSimple() {
    method render (line 55) | render() {

FILE: src/client/Application/TabSection.js
  class TabSection (line 34) | class TabSection extends React.Component {
    method constructor (line 35) | constructor(props) {
    method getComponent (line 64) | getComponent(value) {
    method render (line 68) | render() {

FILE: src/client/Application/URLState.js
  constant SEPARATOR (line 10) | const SEPARATOR = '~';
  function serializeItem (line 12) | function serializeItem(item) {
  function deserializeItem (line 16) | function deserializeItem(token) {
  class URLState (line 21) | class URLState {
    method getStateFromURL (line 22) | static getStateFromURL() {
    method getURLFromState (line 36) | static getURLFromState(state) {
    method init (line 50) | static init() {
    method constructor (line 55) | constructor() {
    method cleanup (line 65) | cleanup() {
    method onChange (line 69) | onChange() {
    method getLink (line 74) | getLink(methodOrData) {
    method onUpdate (line 84) | onUpdate(methodOrData) {

FILE: src/client/Common/AddLinkPlugin.js
  method keyBindingFn (line 44) | keyBindingFn(event, { getEditorState }) {
  method handleKeyCommand (line 56) | handleKeyCommand(command, editorState, eventTimeStamp, { getEditorState,...

FILE: src/client/Common/AsyncSelector.js
  class AsyncSelector (line 7) | class AsyncSelector extends React.Component {
    method constructor (line 8) | constructor(props) {
    method componentDidMount (line 13) | componentDidMount() {
    method componentDidUpdate (line 22) | componentDidUpdate(prevProps) {
    method componentWillUnmount (line 26) | componentWillUnmount() {
    method onChange (line 30) | onChange(id) {
    method render (line 41) | render() {

FILE: src/client/Common/BulletList/BulletList.js
  class BulletList (line 21) | class BulletList extends React.Component {
    method getDerivedStateFromProps (line 22) | static getDerivedStateFromProps(props, state) {
    method constructor (line 30) | constructor(props) {
    method componentDidMount (line 42) | componentDidMount() {
    method componentDidUpdate (line 61) | componentDidUpdate(prevProps) {
    method componentWillUnmount (line 70) | componentWillUnmount() {
    method onAddButtonClick (line 74) | onAddButtonClick(event) {
    method onSortButtonClick (line 82) | onSortButtonClick(event) {
    method onMove (line 90) | onMove(index, delta, event) {
    method onReorder (line 98) | onReorder({ oldIndex, newIndex }) {
    method updateLimit (line 110) | updateLimit(limit) {
    method renderItems (line 114) | renderItems() {
    method renderAdder (line 147) | renderAdder() {
    method render (line 159) | render() {

FILE: src/client/Common/BulletList/BulletListIcon.js
  function BulletListIcon (line 6) | function BulletListIcon(props) {

FILE: src/client/Common/BulletList/BulletListItem.js
  class BulletListItem (line 23) | class BulletListItem extends React.Component {
    method constructor (line 24) | constructor(props) {
    method onEdit (line 30) | onEdit(event) {
    method onDelete (line 48) | onDelete(event) {
    method onKeyDown (line 67) | onKeyDown(event) {
    method getIsExpanded (line 81) | getIsExpanded() {
    method setIsExpanded (line 88) | setIsExpanded(isExpanded) {
    method setIsHighlighted (line 96) | setIsHighlighted(isHighlighted) {
    method getViewerProps (line 103) | getViewerProps() {
    method renderDragHandle (line 107) | renderDragHandle() {
    method renderBullet (line 113) | renderBullet() {
    method renderEditButton (line 134) | renderEditButton() {
    method renderActionsDropdown (line 141) | renderActionsDropdown() {
    method renderExpanded (line 180) | renderExpanded() {
    method renderViewer (line 193) | renderViewer() {
    method renderExpandedViewer (line 203) | renderExpandedViewer() {
    method render (line 211) | render() {

FILE: src/client/Common/BulletList/BulletListLine.js
  function BulletListLine (line 5) | function BulletListLine(props) {

FILE: src/client/Common/BulletList/BulletListPager.js
  class BulletListPager (line 8) | class BulletListPager extends React.Component {
    method constructor (line 9) | constructor(props) {
    method onKeyDown (line 16) | onKeyDown(event) {
    method renderButtons (line 24) | renderButtons() {
    method render (line 45) | render() {

FILE: src/client/Common/BulletList/BulletListTitle.js
  class BulletListTitle (line 11) | class BulletListTitle extends React.Component {
    method constructor (line 12) | constructor(props) {
    method onKeyDown (line 17) | onKeyDown(event) {
    method renderListToggleButton (line 23) | renderListToggleButton() {
    method renderAddButton (line 44) | renderAddButton() {
    method renderSortButton (line 58) | renderSortButton() {
    method render (line 74) | render() {

FILE: src/client/Common/ConfirmModal.js
  function ConfirmModal (line 8) | function ConfirmModal(props) {

FILE: src/client/Common/Coordinator.js
  class Coordinator (line 3) | class Coordinator {
    method register (line 4) | static register(name, callback) {
    method invoke (line 9) | static invoke(name, ...args) {
    method subscribe (line 13) | static subscribe(name, callback) {
    method broadcast (line 24) | static broadcast(name, ...args) {

FILE: src/client/Common/DataLoader.js
  function IGNORE (line 6) | function IGNORE() {
  class DataLoader (line 10) | class DataLoader {
    method constructor (line 11) | constructor({ getInput, onData, onError }) {
    method reload (line 21) | reload({ force } = {}) {
    method _compare (line 54) | _compare(name, left, right) {
    method setupSubscription (line 67) | setupSubscription() {
    method stop (line 87) | stop() {

FILE: src/client/Common/DatePicker.js
  class DatePicker (line 12) | class DatePicker extends React.Component {
    method constructor (line 13) | constructor(props) {
    method render (line 20) | render() {

FILE: src/client/Common/DateRangePicker.js
  constant KEY (line 13) | const KEY = 'selection';
  function DateRangeSelector (line 15) | function DateRangeSelector(props) {
  class DateRangePicker (line 43) | class DateRangePicker extends React.Component {
    method constructor (line 44) | constructor(props) {
    method renderSummary (line 51) | renderSummary() {
    method render (line 62) | render() {
  constant DATE_RANGE_SEPARATOR (line 90) | const DATE_RANGE_SEPARATOR = ' to ';

FILE: src/client/Common/Dropdown.js
  class CustomDropdown (line 9) | class CustomDropdown extends React.Component {
    method constructor (line 10) | constructor(props) {
    method componentDidMount (line 15) | componentDidMount() {
    method onSelect (line 21) | onSelect(item, event) {
    method setIsShown (line 36) | setIsShown(nextIsShown) {
    method hide (line 44) | hide() {
    method show (line 48) | show() {
    method renderItems (line 57) | renderItems() {
    method render (line 76) | render() {

FILE: src/client/Common/EditorModal.js
  class EditorModal (line 10) | class EditorModal extends React.Component {
    method constructor (line 11) | constructor(props) {
    method componentDidMount (line 22) | componentDidMount() {
    method onChange (line 26) | onChange(value) {
    method onSave (line 30) | onSave() {
    method onClose (line 34) | onClose() {
    method validateItemNotDebounced (line 38) | validateItemNotDebounced() {
    method saveItemNotDebounced (line 48) | saveItemNotDebounced() {
    method renderSaveButton (line 72) | renderSaveButton() {
    method render (line 84) | render() {

FILE: src/client/Common/EnumSelectorSection.js
  class EnumSelectorSection (line 7) | class EnumSelectorSection extends React.Component {
    method renderOptions (line 8) | renderOptions() {
    method render (line 28) | render() {

FILE: src/client/Common/ErrorModal.js
  function ErrorModal (line 7) | function ErrorModal(props) {

FILE: src/client/Common/Highlightable.js
  class Highlightable (line 9) | class Highlightable extends React.Component {
    method constructor (line 10) | constructor(props) {
    method componentDidMount (line 15) | componentDidMount() {
    method componentWillUnmount (line 21) | componentWillUnmount() {
    method setHighlight (line 25) | setHighlight(isHighlighted) {
    method render (line 36) | render() {

FILE: src/client/Common/Icon.js
  function Icon (line 7) | function Icon(props) {

FILE: src/client/Common/InfoModal.js
  function InfoModal (line 7) | function InfoModal(props) {

FILE: src/client/Common/InputLine.js
  function InputLine (line 7) | function InputLine(props) {

FILE: src/client/Common/LeftRight.js
  function LeftRight (line 5) | function LeftRight(props) {

FILE: src/client/Common/Link.js
  function Link (line 7) | function Link(props) {

FILE: src/client/Common/ModalStack.js
  class ModalStack (line 10) | class ModalStack extends React.Component {
    method constructor (line 11) | constructor(props) {
    method componentDidMount (line 19) | componentDidMount() {
    method componentWillUnmount (line 31) | componentWillUnmount() {
    method push (line 35) | push(ComponentClass, componentProps) {
    method pop (line 47) | pop(index, callback) {
    method renderItem (line 59) | renderItem({ ComponentClass, componentProps }, index) {
    method render (line 73) | render() {

FILE: src/client/Common/Plugins.js
  class PluginClient (line 9) | class PluginClient {
    method getSettingsKey (line 10) | static getSettingsKey() {
    method getSettingsComponent (line 17) | static getSettingsComponent() {
    method getDisplayLocation (line 23) | static getDisplayLocation() {
    method getDisplayComponent (line 29) | static getDisplayComponent() {
    method getTabData (line 35) | static getTabData() {
  class PluginDisplayComponent (line 60) | class PluginDisplayComponent extends React.Component {
    method renderActual (line 61) | renderActual(settings) {
    method render (line 76) | render() {

FILE: src/client/Common/PopoverElement.js
  class PopoverElement (line 10) | class PopoverElement extends React.Component {
    method renderOverlayTrigger (line 11) | renderOverlayTrigger() {
    method renderButton (line 31) | renderButton() {
    method render (line 35) | render() {

FILE: src/client/Common/ScrollableSection.js
  class WindowHeightDetector (line 8) | class WindowHeightDetector {
    method subscribe (line 9) | static subscribe(callback) {
    method constructor (line 18) | constructor() {
    method onResize (line 24) | onResize() {
  class ScrollableSection (line 30) | class ScrollableSection extends React.Component {
    method constructor (line 31) | constructor(props) {
    method render (line 38) | render() {

FILE: src/client/Common/Selector.js
  class Selector (line 7) | class Selector extends React.Component {
    method constructor (line 8) | constructor(props) {
    method focus (line 13) | focus() {
    method render (line 17) | render() {
  class BinarySelector (line 53) | class BinarySelector extends React.Component {
    method constructor (line 54) | constructor(props) {
    method focus (line 59) | focus() {
    method render (line 63) | render() {

FILE: src/client/Common/SidebarSection.js
  class SidebarSection (line 13) | class SidebarSection extends React.Component {
    method constructor (line 14) | constructor(props) {
    method renderHeader (line 19) | renderHeader() {
    method renderChildren (line 39) | renderChildren() {
    method render (line 50) | render() {

FILE: src/client/Common/SortableList.js
  class SortableList (line 47) | class SortableList extends React.Component {
    method constructor (line 48) | constructor(props) {
    method onReorder (line 57) | onReorder({ oldIndex, newIndex }) {
    method onChange (line 61) | onChange(index, item) {
    method onDelete (line 67) | onDelete(index) {
    method renderRow (line 73) | renderRow(item, index) {
    method render (line 95) | render() {

FILE: src/client/Common/StandardIcons.js
  function getIconWrapper (line 7) | function getIconWrapper(Component, color = 'var(--link-color)', style = ...

FILE: src/client/Common/TextEditor.js
  function MentionComponent (line 19) | function MentionComponent(props) {
  function OptionComponent (line 30) | function OptionComponent(props) {
  class TextEditor (line 46) | class TextEditor extends React.Component {
    method getDerivedStateFromProps (line 47) | static getDerivedStateFromProps(props, state) {
    method constructor (line 63) | constructor(props) {
    method handleKeyCommand (line 110) | handleKeyCommand(command, editorState) {
    method onSearchChange (line 122) | onSearchChange({ value: query }) {
    method onAddMention (line 129) | onAddMention(option) {
    method onChange (line 148) | onChange(editorState) {
    method focus (line 160) | focus() {
    method keyBindingFn (line 167) | keyBindingFn(event) {
    method renderSuggestions (line 179) | renderSuggestions() {
    method render (line 197) | render() {

FILE: src/client/Common/TextInput.js
  class TextInput (line 5) | class TextInput extends React.Component {
    method constructor (line 6) | constructor(props) {
    method focus (line 11) | focus() {
    method render (line 15) | render() {

FILE: src/client/Common/TooltipElement.js
  function TooltipElement (line 6) | function TooltipElement(props) {

FILE: src/client/Common/TypeaheadInput.js
  class TypeaheadInput (line 5) | class TypeaheadInput extends React.Component {
    method constructor (line 6) | constructor(props) {
    method onSearch (line 12) | onSearch(query) {
    method focus (line 19) | focus() {
    method render (line 23) | render() {

FILE: src/client/Common/TypeaheadOptions.js
  class TypeaheadOptions (line 3) | class TypeaheadOptions {
    method getFromTypes (line 4) | static getFromTypes(names) {
    method constructor (line 10) | constructor(config) {
    method search (line 31) | async search(query, existingItems) {
    method select (line 82) | async select(option) {
    method filterToKnownTypes (line 103) | filterToKnownTypes(items) {

FILE: src/client/Common/TypeaheadSelector.js
  class TypeaheadSelector (line 12) | class TypeaheadSelector extends React.Component {
    method constructor (line 13) | constructor(props) {
    method onInputChange (line 19) | onInputChange(text) {
    method onSearch (line 24) | onSearch(query) {
    method onChange (line 37) | async onChange(selected) {
    method focus (line 54) | focus() {
    method renderDeleteButton (line 58) | renderDeleteButton() {
    method render (line 72) | render() {

FILE: src/client/Common/URLManager.js
  class URLManager (line 9) | class URLManager {
    method init (line 10) | static init(callback) {
    method get (line 25) | static get() {
    method getLink (line 29) | static getLink(params) {
    method update (line 33) | static update(link) {

FILE: src/client/Common/Utils.js
  function suppressUnlessShiftKey (line 1) | function suppressUnlessShiftKey(event) {
  function debounce (line 8) | function debounce(func, wait, immediate) {

FILE: src/client/Graphs/GraphLineChart.js
  function GraphLineChart (line 9) | function GraphLineChart(props) {

FILE: src/client/Graphs/GraphSection.js
  class GraphSection (line 13) | class GraphSection extends React.Component {
    method getTypeaheadOptions (line 14) | static getTypeaheadOptions() {
    method getDerivedStateFromProps (line 18) | static getDerivedStateFromProps(props, state) {
    method constructor (line 39) | constructor(props) {
    method componentDidMount (line 44) | componentDidMount() {
    method componentDidUpdate (line 75) | componentDidUpdate(prevProps) {
    method componentWillUnmount (line 83) | componentWillUnmount() {
    method render (line 87) | render() {

FILE: src/client/Graphs/GraphSectionData.js
  function getLogKeyValues (line 8) | function getLogKeyValues(keyIndex, valueParser, logEvents) {
  function getAverageValue (line 18) | function getAverageValue(values) {
  function getLines (line 28) | function getLines(logStructure, logEvent) {
  function getTimeSeries (line 60) | function getTimeSeries(logEvents, lines, dateRange, granularity) {
  function getGraphData (line 121) | function getGraphData(logStructure, logEvents, dateRange, granularity) {

FILE: src/client/Graphs/GraphSectionOptions.js
  constant GRANULARITY_TYPE (line 35) | const GRANULARITY_TYPE = 'graph-granularity';
  constant GRANULARITY_PREFIX (line 36) | const GRANULARITY_PREFIX = 'Granularity: ';
  constant GRANULARITY_OPTIONS (line 37) | const GRANULARITY_OPTIONS = Granularity.Options.map((option, index) => ({
  constant GRANULARITY_MOCK_OPTION (line 43) | const GRANULARITY_MOCK_OPTION = {
  class GraphSectionOptions (line 50) | class GraphSectionOptions {
    method get (line 51) | static get() {
    method extractData (line 55) | static extractData(items) {

FILE: src/client/Graphs/GraphTooltip.js
  function NormalTooltip (line 4) | function NormalTooltip({ active, label, payload }) {

FILE: src/client/LogEvent/LogEventAdder.js
  class LogEventAdder (line 10) | class LogEventAdder extends React.Component {
    method constructor (line 11) | constructor(props) {
    method onEditLogEvent (line 18) | onEditLogEvent(logEvent) {
    method onSaveLogEvent (line 28) | onSaveLogEvent(logEvent) {
    method onSelect (line 40) | async onSelect(option) {
    method render (line 56) | render() {

FILE: src/client/LogEvent/LogEventDetailsHeader.js
  class LogEventDetailsHeader (line 8) | class LogEventDetailsHeader extends React.Component {
    method renderTitle (line 9) | renderTitle() {
    method render (line 23) | render() {

FILE: src/client/LogEvent/LogEventEditor.js
  class LogEventEditor (line 13) | class LogEventEditor extends React.Component {
    method constructor (line 14) | constructor(props) {
    method componentDidMount (line 21) | componentDidMount() {
    method updateLogEvent (line 34) | updateLogEvent(methodOrName, maybeValue) {
    method renderDate (line 45) | renderDate() {
    method renderIsComplete (line 60) | renderIsComplete() {
    method renderTitle (line 75) | renderTitle() {
    method renderDetails (line 102) | renderDetails() {
    method renderLogLevel (line 123) | renderLogLevel() {
    method renderStructureSelector (line 139) | renderStructureSelector() {
    method renderStructureValues (line 167) | renderStructureValues() {
    method render (line 185) | render() {

FILE: src/client/LogEvent/LogEventList.js
  function LogEventViewer (line 8) | function LogEventViewer(props) {
  function LogEventList (line 79) | function LogEventList(props) {

FILE: src/client/LogEvent/LogEventOptions.js
  constant NO_STRUCTURE_ITEM (line 6) | const NO_STRUCTURE_ITEM = {
  constant EVENT_TITLE_ITEM_TYPE (line 12) | const EVENT_TITLE_ITEM_TYPE = 'log-event-title';
  constant EVENT_TITLE_ITEM_PREFIX (line 13) | const EVENT_TITLE_ITEM_PREFIX = 'Title: ';
  class LogEventOptions (line 15) | class LogEventOptions {
    method get (line 16) | static get(prefixOptions) {
    method getTypeToActionMap (line 45) | static getTypeToActionMap(extraOptions) {
    method extractData (line 74) | static extractData(items, typeToActionMap, defaultWhere) {

FILE: src/client/LogEvent/LogEventSearch.js
  constant INCOMPLETE_ITEM (line 15) | const INCOMPLETE_ITEM = {
  constant LOG_LEVEL_MINOR_ITEM (line 24) | const LOG_LEVEL_MINOR_ITEM = {
  constant LOG_LEVEL_MAJOR_ITEM (line 29) | const LOG_LEVEL_MAJOR_ITEM = {
  constant LOG_LEVEL_MOCK_ITEM (line 34) | const LOG_LEVEL_MOCK_ITEM = {
  constant DEFAULT_WHERE (line 47) | const DEFAULT_WHERE = {
  function getDayOfTheWeek (line 52) | function getDayOfTheWeek(label) {
  class LogEventSearch (line 56) | class LogEventSearch extends React.Component {
    method getTypeaheadOptions (line 57) | static getTypeaheadOptions() {
    method getDerivedStateFromProps (line 65) | static getDerivedStateFromProps(props, _state) {
    method constructor (line 76) | constructor(props) {
    method componentDidMount (line 81) | componentDidMount() {
    method componentWillUnmount (line 91) | componentWillUnmount() {
    method getPlanForTodayAction (line 97) | getPlanForTodayAction() {
    method getCompleteAction (line 112) | getCompleteAction() {
    method getDuplicateAction (line 127) | getDuplicateAction() {
    method renderDefaultView (line 147) | renderDefaultView(where, moreProps, settings) {
    method renderMultipleDaysView (line 222) | renderMultipleDaysView(where, moreProps) {
    method renderSearchView (line 240) | renderSearchView(where, moreProps) {
    method renderIncompleteView (line 274) | renderIncompleteView(where, moreProps) {
    method render (line 298) | render() {

FILE: src/client/LogKey/LogKeyEditor.js
  class LogKeyEditor (line 11) | class LogKeyEditor extends React.Component {
    method getDerivedStateFromProps (line 12) | static getDerivedStateFromProps(props) {
    method constructor (line 18) | constructor(props) {
    method update (line 23) | update(methodOrName, maybeValue) {
    method updateType (line 33) | updateType(newType) {
    method renderTypeSelector (line 40) | renderTypeSelector() {
    method renderNameInput (line 52) | renderNameInput() {
    method renderParentLogTopic (line 63) | renderParentLogTopic() {
    method renderOptionalSelector (line 76) | renderOptionalSelector() {
    method renderValue (line 89) | renderValue() {
    method renderKeyTemplate (line 103) | renderKeyTemplate() {
    method renderEnumValuesSelector (line 118) | renderEnumValuesSelector() {
    method renderEnumValuesSection (line 135) | renderEnumValuesSection() {
    method render (line 146) | render() {

FILE: src/client/LogKey/LogKeyListEditor.js
  class LogKeyListEditor (line 11) | class LogKeyListEditor extends React.Component {
    method renderTitleTemplateEditor (line 12) | renderTitleTemplateEditor() {
    method renderSortableList (line 40) | renderSortableList() {
    method render (line 53) | render() {

FILE: src/client/LogKey/LogValueEditor.js
  class LogValueEditor (line 10) | class LogValueEditor extends React.Component {
    method constructor (line 11) | constructor(props) {
    method focus (line 16) | focus() {
    method update (line 20) | update(value) {
    method render (line 29) | render() {

FILE: src/client/LogKey/LogValueListEditor.js
  class LogValueListEditor (line 7) | class LogValueListEditor extends React.Component {
    method constructor (line 8) | constructor(props) {
    method focus (line 13) | focus() {
    method render (line 17) | render() {

FILE: src/client/LogStructure/LogStructureDetailsHeader.js
  class LogStructureDetailsHeader (line 8) | class LogStructureDetailsHeader extends React.Component {
    method onSearchButtonClick (line 9) | static onSearchButtonClick(logStructure) {
    method render (line 13) | render() {

FILE: src/client/LogStructure/LogStructureEditor.js
  class LogStructureEditor (line 14) | class LogStructureEditor extends React.Component {
    method constructor (line 15) | constructor(props) {
    method componentDidMount (line 20) | componentDidMount() {
    method updateLogStructure (line 24) | updateLogStructure(methodOrName, maybeValue) {
    method renderGroup (line 35) | renderGroup() {
    method renderName (line 59) | renderName() {
    method renderEventNeedsEditSelector (line 75) | renderEventNeedsEditSelector() {
    method renderEventAllowDetailsSelector (line 90) | renderEventAllowDetailsSelector() {
    method renderLogLevelSelector (line 105) | renderLogLevelSelector() {
    method renderIsDeprecated (line 121) | renderIsDeprecated() {
    method render (line 136) | render() {

FILE: src/client/LogStructure/LogStructureFrequencyEditor.js
  class LogStructureFrequencyEditor (line 28) | class LogStructureFrequencyEditor extends React.Component {
    method updateIsPeriodic (line 29) | updateIsPeriodic(newIsPeriodic) {
    method updateFrequency (line 55) | updateFrequency(newFrequency) {
    method renderIsPeriodic (line 71) | renderIsPeriodic() {
    method renderReminderText (line 86) | renderReminderText() {
    method renderFrequency (line 101) | renderFrequency() {
    method renderFrequencyArgs (line 117) | renderFrequencyArgs() {
    method renderWarningDays (line 144) | renderWarningDays() {
    method renderSuppressUntilDate (line 160) | renderSuppressUntilDate() {
    method render (line 175) | render() {

FILE: src/client/LogStructure/LogStructureGroupEditor.js
  class LogStructureGroupEditor (line 7) | class LogStructureGroupEditor extends React.Component {
    method constructor (line 8) | constructor(props) {
    method componentDidMount (line 13) | componentDidMount() {
    method updateLogStructureGroup (line 17) | updateLogStructureGroup(methodOrName, maybeValue) {
    method renderName (line 27) | renderName() {
    method render (line 43) | render() {

FILE: src/client/LogStructure/LogStructureGroupList.js
  function LogStructureGroupViewer (line 8) | function LogStructureGroupViewer(props) {
  function LogStructureGroupList (line 34) | function LogStructureGroupList(props) {

FILE: src/client/LogStructure/LogStructureList.js
  function LogStructureViewer (line 9) | function LogStructureViewer(props) {
  function LogStructureList (line 52) | function LogStructureList(props) {

FILE: src/client/LogStructure/LogStructureOptions.js
  class LogStructureOptions (line 5) | class LogStructureOptions {
    method get (line 6) | static get() {
    method getTypeToActionMap (line 15) | static getTypeToActionMap() {
    method extractData (line 32) | static extractData(items, typeToActionMap) {

FILE: src/client/LogStructure/LogStructureSearch.js
  class LogStructureSearch (line 8) | class LogStructureSearch extends React.Component {
    method getTypeaheadOptions (line 9) | static getTypeaheadOptions() {
    method getDerivedStateFromProps (line 13) | static getDerivedStateFromProps(props, _state) {
    method constructor (line 20) | constructor(props) {
    method renderSearchView (line 25) | renderSearchView() {
    method renderDefaultView (line 38) | renderDefaultView() {
    method render (line 47) | render() {

FILE: src/client/LogTopic/LogTopicDetailsHeader.js
  class LogTopicDetailsHeader (line 9) | class LogTopicDetailsHeader extends React.Component {
    method onSearchButtonClick (line 10) | static onSearchButtonClick(logTopic) {
    method renderParentTopic (line 14) | renderParentTopic() {
    method renderChildTopics (line 29) | renderChildTopics() {
    method render (line 51) | render() {

FILE: src/client/LogTopic/LogTopicEditor.js
  class LogTopicEditor (line 11) | class LogTopicEditor extends React.Component {
    method constructor (line 12) | constructor(props) {
    method componentDidMount (line 17) | componentDidMount() {
    method updateLogTopic (line 21) | updateLogTopic(methodOrName, maybeValue) {
    method renderParent (line 32) | renderParent() {
    method renderName (line 53) | renderName() {
    method renderIsDeprecated (line 71) | renderIsDeprecated() {
    method renderValues (line 86) | renderValues() {
    method renderChildKeys (line 103) | renderChildKeys() {
    method render (line 147) | render() {

FILE: src/client/LogTopic/LogTopicList.js
  function LogTopicViewer (line 9) | function LogTopicViewer(props) {
  function LogTopicList (line 35) | function LogTopicList(props) {

FILE: src/client/LogTopic/LogTopicOptions.js
  constant CREATE_ITEM (line 7) | const CREATE_ITEM = {
  method getItem (line 11) | getItem(_option, partialParentLogTopic) {
  class LogTopicOptions (line 31) | class LogTopicOptions {
    method get (line 32) | static get({
    method getTypeToActionMap (line 50) | static getTypeToActionMap() {
    method extractData (line 62) | static extractData(items, typeToActionMap) {

FILE: src/client/LogTopic/LogTopicSearch.js
  class LogTopicSearch (line 8) | class LogTopicSearch extends React.Component {
    method getTypeaheadOptions (line 9) | static getTypeaheadOptions() {
    method getDerivedStateFromProps (line 18) | static getDerivedStateFromProps(props, _state) {
    method constructor (line 25) | constructor(props) {
    method renderSearchView (line 30) | renderSearchView() {
    method renderDefaultView (line 53) | renderDefaultView() {
    method render (line 57) | render() {

FILE: src/client/Reminders/ReminderItem.js
  class ReminderItem (line 14) | class ReminderItem extends React.Component {
    method constructor (line 15) | constructor(props) {
    method onEditButtonClick (line 21) | onEditButtonClick() {
    method onCompleteReminder (line 30) | onCompleteReminder(logEvent = null) {
    method onDismissReminder (line 57) | onDismissReminder() {
    method renderRight (line 63) | renderRight() {
    method render (line 117) | render() {

FILE: src/client/Reminders/ReminderList.js
  class ReminderList (line 7) | class ReminderList extends React.Component {
    method renderContent (line 8) | renderContent() {
    method render (line 20) | render() {

FILE: src/client/Reminders/ReminderSidebar.js
  class ReminderSidebar (line 7) | class ReminderSidebar extends React.Component {
    method constructor (line 8) | constructor(props) {
    method componentDidMount (line 13) | componentDidMount() {
    method componentDidUpdate (line 21) | componentDidUpdate() {
    method componentWillUnmount (line 25) | componentWillUnmount() {
    method render (line 29) | render() {

FILE: src/client/Settings/SettingsEditor.js
  constant SETTINGS_ITEMS (line 10) | const SETTINGS_ITEMS = [
  class SettingsEditor (line 42) | class SettingsEditor extends React.Component {
    method getSetting (line 43) | getSetting(key, defaultValue = null) {
    method setSetting (line 47) | setSetting(key, value) {
    method renderSettingsItems (line 53) | renderSettingsItems() {
    method render (line 95) | render() {

FILE: src/client/Settings/SettingsModal.js
  class SettingsModal (line 11) | class SettingsModal extends React.Component {
    method constructor (line 12) | constructor(props) {
    method onSave (line 20) | onSave() {
    method render (line 26) | render() {

FILE: src/client/Settings/SettingsSection.js
  class SettingsSection (line 7) | class SettingsSection extends React.Component {
    method constructor (line 8) | constructor(props) {
    method componentDidMount (line 15) | componentDidMount() {
    method componentWillUnmount (line 23) | componentWillUnmount() {
    method render (line 27) | render() {

FILE: src/client/index.js
  function getCookies (line 14) | function getCookies() {

FILE: src/common/AsyncUtils.js
  function asyncSequence (line 1) | function asyncSequence(items, method) {
  function asyncFilter (line 26) | function asyncFilter(items, method) {
  function callbackToPromise (line 42) | function callbackToPromise(method, ...args) {

FILE: src/common/DateUtils.js
  constant MS_IN_HOURS (line 6) | const MS_IN_HOURS = 60 * 60 * 1000;
  constant LABEL_FORMAT (line 7) | const LABEL_FORMAT = 'yyyy-MM-dd';
  class DateUtils (line 40) | class DateUtils {
    method getContext (line 41) | static getContext(settings) {
    method getDate (line 53) | static getDate(label) {
    method getLabel (line 57) | static getLabel(date) {
    method maybeSubstitute (line 61) | static maybeSubstitute(todayDate, path, name) {

FILE: src/common/RichTextUtils.js
  function toString (line 14) | function toString(value) {
  constant DRAFTJS_MENTION_PLUGIN_NAME (line 21) | const DRAFTJS_MENTION_PLUGIN_NAME = 'mention';
  constant DRAFTJS_MENTION_ENTITY_TYPE (line 22) | const DRAFTJS_MENTION_ENTITY_TYPE = 'mention';
  constant MARKDOWN_MENTION_PREFIX (line 23) | const MARKDOWN_MENTION_PREFIX = 'mention';
  constant LINK_ENTITY_TYPE (line 25) | const LINK_ENTITY_TYPE = 'LINK';
  method open (line 30) | open(entity) {
  method close (line 33) | close(entity) {
  function postProcessDraftRawContent (line 40) | function postProcessDraftRawContent(rawContent) {
  class RichTextUtils (line 56) | class RichTextUtils {
    method extractPlainText (line 58) | static extractPlainText(value) {
    method equals (line 68) | static equals(left, right) {
    method deserialize (line 78) | static deserialize(value, type) {
    method serialize (line 106) | static serialize(value, type) {
    method fromEditorState (line 138) | static fromEditorState(editorState) {
    method toEditorState (line 143) | static toEditorState(value) {
    method getSelectionData (line 150) | static getSelectionData(editorState) {
    method setSelectionData (line 172) | static setSelectionData(editorState, data) {
    method fixCursorBug (line 187) | static fixCursorBug(prevEditorState, nextEditorState) {
    method convertPlainTextToDraftContent (line 207) | static convertPlainTextToDraftContent(value, symbolToItems) {
    method convertDraftContentToPlainText (line 228) | static convertDraftContentToPlainText(value, symbolToItems) {
    method extractMentions (line 259) | static extractMentions(content, type) {
    method updateDraftContent (line 277) | static updateDraftContent(content, oldItems, newItems, evaluateExpress...
    method addPrefixToDraftContent (line 401) | static addPrefixToDraftContent(contentState, items) {
    method removePrefixFromDraftContext (line 444) | static removePrefixFromDraftContext(content, prefix) {
    method evaluateDraftContentExpressions (line 463) | static evaluateDraftContentExpressions(contentState) {

FILE: src/common/SocketRPC.js
  constant SERVER_SIDE (line 3) | const SERVER_SIDE = 'server_side';
  constant CLIENT_SIDE (line 4) | const CLIENT_SIDE = 'client_side';
  constant GENERAL_REQUEST (line 6) | const GENERAL_REQUEST = 'general-request-';
  constant GENERAL_RESPONSE (line 7) | const GENERAL_RESPONSE = 'general-response-';
  constant GENERAL_SUBSCRIPTION (line 8) | const GENERAL_SUBSCRIPTION = 'general-subscription';
  constant LOG_SUBSCRIPTION (line 9) | const LOG_SUBSCRIPTION = 'log-subscription';
  function _remove (line 11) | function _remove(list, value) {
  class SocketRPC (line 16) | class SocketRPC {
    method server (line 19) | static server(socket, actions) {
    method client (line 26) | static client(socket, thenCallback, catchCallback) {
    method constructor (line 32) | constructor(type, socket, thenCallback, catchCallback) {
    method send (line 48) | send(name, request) {
    method registerActions (line 75) | registerActions(actions) {
    method registerSubscriptions (line 105) | registerSubscriptions() {
    method subscribe (line 128) | subscribe(name) {
    method broadcast (line 142) | broadcast(name, data) {
    method log (line 153) | log(...args) {

FILE: src/common/__tests__/RichTextUtils.test.js
  function verify (line 10) | function verify(inputType, outputType) {

FILE: src/common/data_types/LogEvent.js
  class LogEvent (line 12) | class LogEvent extends DataTypeBase {
    method createVirtual (line 13) | static createVirtual({
    method addDefaultStructureValues (line 47) | static addDefaultStructureValues(logEvent) {
    method updateWhere (line 58) | static async updateWhere(where) {
    method trigger (line 93) | static trigger(logEvent) {
    method updateLogTopicsInTitleAndDetails (line 120) | static async updateLogTopicsInTitleAndDetails(inputLogEvent) {
    method updateLogTopics (line 145) | static async updateLogTopics(inputLogEvent) {
    method validate (line 157) | static async validate(inputLogEvent) {
    method load (line 205) | static async load(id) {
    method save (line 237) | static async save(inputLogEvent) {
    method delete (line 301) | static async delete(id) {

FILE: src/common/data_types/LogKey.js
  class LogKey (line 72) | class LogKey {
    method createVirtual (line 73) | static createVirtual() {
    method validate (line 86) | static async validate(inputLogKey) {
    method validateValue (line 106) | static async validateValue(inputLogKey, index) {
    method load (line 122) | static async load(rawLogKey, index) {
    method save (line 145) | static save(inputLogKey) {
    method updateLogTopicsInLogTopicType (line 165) | static async updateLogTopicsInLogTopicType(inputLogKey) {
    method updateLogTopicsInRichTextLineType (line 185) | static async updateLogTopicsInRichTextLineType(inputLogKey) {
    method updateLogTopics (line 202) | static async updateLogTopics(inputLogKey) {

FILE: src/common/data_types/LogStructure.js
  class LogStructure (line 32) | class LogStructure extends DataTypeBase {
    method createVirtual (line 33) | static createVirtual({ logStructureGroup, name = '' }) {
    method updateWhere (line 56) | static async updateWhere(where) {
    method trigger (line 67) | static trigger(logStructure) {
    method updateLogTopicsInTitleTemplateAndDetails (line 85) | static async updateLogTopicsInTitleTemplateAndDetails(inputLogStructur...
    method updateLogTopics (line 110) | static async updateLogTopics(inputLogStructure) {
    method validate (line 122) | static async validate(inputLogStructure) {
    method load (line 176) | static async load(id) {
    method save (line 216) | static async save(inputLogStructure) {
    method delete (line 363) | static async delete(id) {

FILE: src/common/data_types/LogStructureFrequency.js
  method getPreviousMatch (line 17) | getPreviousMatch(date) {
  method getNextMatch (line 20) | getNextMatch(date) {
  method getPreviousMatch (line 27) | getPreviousMatch(date) {
  method getNextMatch (line 35) | getNextMatch(date) {
  method getPreviousMatch (line 47) | getPreviousMatch(date) {
  method getNextMatch (line 53) | getNextMatch(date) {
  method getPreviousMatch (line 67) | getPreviousMatch(date) {
  method getNextMatch (line 71) | getNextMatch(date) {
  function parseYearlyFrequencyArgs (line 78) | function parseYearlyFrequencyArgs(args) {
  method getPreviousMatch (line 88) | getPreviousMatch(date, args) {
  method getNextMatch (line 96) | getNextMatch(date, args) {

FILE: src/common/data_types/LogStructureGroup.js
  class LogStructureGroup (line 5) | class LogStructureGroup extends DataTypeBase {
    method createVirtual (line 6) | static createVirtual() {
    method updateWhere (line 14) | static async updateWhere(where) {
    method validate (line 20) | static async validate(inputLogStructureGroup) {
    method load (line 28) | static async load(id) {
    method save (line 37) | static async save(inputLogStructureGroup) {
    method updateLogStructures (line 58) | static async updateLogStructures(inputLogStructureGroup) {
    method delete (line 69) | static async delete(id) {

FILE: src/common/data_types/LogTopic.js
  class LogTopic (line 8) | class LogTopic extends DataTypeBase {
    method createVirtual (line 9) | static createVirtual({ parentLogTopic = null, name = '' } = {}) {
    method updateWhere (line 30) | static async updateWhere(where) {
    method trigger (line 39) | static trigger(inputLogTopic) {
    method updateLogTopicInDetails (line 54) | static async updateLogTopicInDetails(inputLogTopic) {
    method updateLogTopics (line 73) | static async updateLogTopics(inputLogTopic) {
    method validate (line 85) | static async validate(inputLogTopic) {
    method loadPartial (line 114) | static async loadPartial(id) {
    method load (line 123) | static async load(id) {
    method save (line 183) | static async save(inputLogTopic) {
    method updateOtherEntities (line 360) | static async updateOtherEntities(
    method delete (line 383) | static async delete(id) {
    method sort (line 403) | static async sort(input) {

FILE: src/common/data_types/__tests__/LogStructureFrequency.test.js
  function check (line 23) | function check(frequency, date1, method, date2, args = null) {

FILE: src/common/data_types/api.js
  class DataTypeAPI (line 1) | class DataTypeAPI {
    method createVirtual (line 4) | static createVirtual() { // create
    method trigger (line 10) | static trigger(_item) { // update
    method validate (line 16) | static async validate(_item) {
    method where (line 22) | static async where(_fields) { // filter, search
    method load (line 28) | static async load(_id) {
    method save (line 34) | static async save(_item) {
    method delete (line 40) | static async delete(_id) {

FILE: src/common/data_types/base.js
  function getDataType (line 7) | function getDataType(name) {
  class DataTypeBase (line 11) | class DataTypeBase extends DataTypeAPI {
    method getValidationErrors (line 12) | static async getValidationErrors(inputItem) {
    method updateLogTopicsWhere (line 25) | static async updateLogTopicsWhere(where) {
    method updateWhere (line 60) | static async updateWhere(where, mapping) {
    method trigger (line 78) | static trigger(item) {
    method list (line 82) | static async list(where, limit) {
    method count (line 93) | static async count(where) {
    method typeahead (line 98) | static async typeahead({ query, where }) {
    method reorder (line 134) | static async reorder(input) {
    method getOrderingIndex (line 147) | static async getOrderingIndex(item, where = {}) {
    method broadcast (line 154) | static async broadcast(queryName, prevItem, fields) {

FILE: src/common/data_types/enum.js
  function Enum (line 3) | function Enum(Options) {

FILE: src/common/data_types/index.js
  function getDataTypeMapping (line 22) | function getDataTypeMapping() {

FILE: src/common/data_types/utils.js
  function getVirtualID (line 3) | function getVirtualID() {
  function isItem (line 8) | function isItem(item) {
  function isVirtualItem (line 12) | function isVirtualItem(item) {
  function isRealItem (line 16) | function isRealItem(item) {
  function getPartialItem (line 20) | function getPartialItem(item) {
  function getNextID (line 24) | function getNextID(items) {

FILE: src/common/data_types/validation.js
  function validateNonEmptyString (line 3) | function validateNonEmptyString(name, value) {
  function validateIndex (line 18) | function validateIndex(name, value) {
  function validateEnumValue (line 33) | function validateEnumValue(name, value, Enum) {
  function validateDateLabel (line 41) | function validateDateLabel(name, label) {
  function validateRecursive (line 49) | async function validateRecursive(DataType, name, item) {
  function validateRecursiveList (line 58) | async function validateRecursiveList(DataType, name, items) {

FILE: src/common/polyfill.js
  function getTimePrefix (line 1) | function getTimePrefix() {
  function addTimePrefix (line 6) | function addTimePrefix(name) {

FILE: src/demo/components/Application.js
  class Application (line 9) | class Application extends BaseWrapper {
    method constructor (line 10) | constructor(webdriver) {
    method switchToTab (line 15) | async switchToTab(name) {
    method getSidebarSection (line 22) | async getSidebarSection(...args) {
    method getIndexSection (line 26) | async getIndexSection() {
    method getDetailsSection (line 31) | async getDetailsSection(...args) {
    method getModalDialog (line 35) | async getModalDialog(...args) {
    method getTopic (line 39) | async getTopic(name, index) {
    method getLink (line 45) | async getLink(name, index = 0) {
    method isDetailsSectionActive (line 53) | async isDetailsSectionActive() {
    method performCreateNew (line 58) | async performCreateNew(bulletList) {
    method performInputName (line 65) | async performInputName(name) {
    method clearDatabase (line 72) | async clearDatabase() {
    method waitUntil (line 78) | async waitUntil(conditionMethod) {
    method scrollToBottom (line 83) | async scrollToBottom(className, index) {

FILE: src/demo/components/BaseWrapper.js
  class BaseWrapper (line 6) | class BaseWrapper {
    method constructor (line 7) | constructor(webdriver, element) {
    method getInput (line 14) | async getInput() {
    method sendKeys (line 19) | async sendKeys(...items) {
    method typeSlowly (line 40) | async typeSlowly(text) {
    method _moveTo (line 53) | async _moveTo(element) {
    method moveTo (line 57) | async moveTo(element) {
    method _click (line 62) | async _click(element) {
    method click (line 66) | async click(element) {
    method moveToAndClick (line 71) | async moveToAndClick(element) {
    method wait (line 77) | wait(milliseconds = 250) {
    method getItemByIndex (line 83) | static getItemByIndex(items, index) {
    method getElementByClassName (line 87) | static async getElementByClassName(element, className, index = 0) {

FILE: src/demo/components/BulletList.js
  class BulletList (line 9) | class BulletList extends BaseWrapper {
    method get (line 10) | static async get(webdriver, index) {
    method getHeader (line 16) | async getHeader() {
    method _getItems (line 22) | async _getItems() {
    method getItem (line 26) | async getItem(index) {
    method getItemCount (line 32) | async getItemCount() {
    method getAdder (line 37) | async getAdder() {
  class BulletListItem (line 43) | class BulletListItem extends BaseWrapper {
    method _getButton (line 44) | async _getButton(title) {
    method perform (line 53) | async perform(name) {
    method performAction (line 58) | async performAction(name) {
    method getSubList (line 69) | async getSubList() {
    method move (line 77) | async move(direction) {

FILE: src/demo/components/DetailsSection.js
  class DetailsSection (line 6) | class DetailsSection extends BaseWrapper {
    method get (line 7) | static async get(webdriver, index) {
    method isActive (line 13) | async isActive() {
    method getInput (line 18) | async getInput() {
    method perform (line 22) | async perform(name) {

FILE: src/demo/components/IndexSection.js
  class IndexSection (line 7) | class IndexSection extends BaseWrapper {
    method get (line 8) | static async get(webdriver) {
    method getTypeahead (line 13) | async getTypeahead() {
    method getBulletList (line 20) | async getBulletList(index) {

FILE: src/demo/components/Inputs.js
  class Selector (line 8) | class Selector extends BaseWrapper {
    method get (line 9) | static async get(webdriver, element) {
    method pickOption (line 14) | async pickOption(name) {
  class TypeaheadSelector (line 27) | class TypeaheadSelector extends BaseWrapper {
    method get (line 28) | static async get(webdriver, element) {
    method getTokens (line 33) | async getTokens() {
    method removeToken (line 41) | async removeToken(name) {
    method getInput (line 49) | async getInput() {
    method _getSuggestions (line 61) | async _getSuggestions() {
    method pickSuggestion (line 70) | async pickSuggestion(label) {
  class TextEditor (line 81) | class TextEditor extends BaseWrapper {
    method get (line 82) | static async get(webdriver, element) {
    method getInput (line 87) | async getInput() {
    method getSuggestions (line 93) | async getSuggestions() {
    method pickSuggestion (line 100) | async pickSuggestion(indexOrLabel) {
  class LogStructureKey (line 115) | class LogStructureKey extends BaseWrapper {
    method get (line 116) | static async get(webdriver, element, index) {
    method getTypeSelector (line 122) | async getTypeSelector() {
    method getNameInput (line 126) | async getNameInput() {
    method getTemplateInput (line 130) | async getTemplateInput() {

FILE: src/demo/components/ModalDialog.js
  class ModalDialog (line 9) | class ModalDialog extends BaseWrapper {
    method get (line 10) | static async get(webdriver, index) {
    method _clickAndWaitForClose (line 16) | async _clickAndWaitForClose(buttonElement) {
    method performClose (line 31) | async performClose() {
    method _getElement (line 39) | async _getElement(name) {
    method getTextInput (line 48) | async getTextInput(name) {
    method getTextEditor (line 53) | async getTextEditor(name) {
    method getTypeahead (line 58) | async getTypeahead(name) {
    method getSelector (line 63) | async getSelector(name) {
    method performSave (line 68) | async performSave() {
    method addLogStructureKey (line 78) | async addLogStructureKey() {
    method getLogStructureKey (line 86) | async getLogStructureKey(index) {
    method getDebugInfo (line 92) | async getDebugInfo() {

FILE: src/demo/components/ReminderItem.js
  class ReminderItem (line 5) | class ReminderItem extends BaseWrapper {
    method getCheckbox (line 6) | async getCheckbox() {
    method pickMenuItem (line 11) | async pickMenuItem(label) {

FILE: src/demo/components/SidebarSection.js
  class SidebarSection (line 6) | class SidebarSection extends BaseWrapper {
    method get (line 7) | static async get(webdriver, name) {
    method getItems (line 17) | async getItems() {
    method getReminderItems (line 24) | async getReminderItems() {

FILE: src/demo/index.js
  constant DEVICE_TO_WINDOW_SPEC (line 12) | const DEVICE_TO_WINDOW_SPEC = {
  function main (line 17) | async function main(argv) {

FILE: src/demo/process.js
  class ProcessWrapper (line 6) | class ProcessWrapper {
    method constructor (line 7) | constructor(args) {
    method start (line 17) | async start() {
    method _onData (line 35) | _onData(data) {
    method waitUntilOutput (line 48) | async waitUntilOutput(text) {
    method waitUntilPause (line 63) | async waitUntilPause(duration) {
    method waitUntilExit (line 77) | async waitUntilExit() {
    method stop (line 84) | async stop() {
  class StreamIntender (line 93) | class StreamIntender {
    method constructor (line 94) | constructor(stream, prefix) {
    method write (line 100) | write(data) {

FILE: src/plugins/kaustubh/long_term_goals/LongTermGoalGraph.js
  constant CURRENT_KEY (line 11) | const CURRENT_KEY = '__current__';
  constant TARGET_KEY (line 12) | const TARGET_KEY = '__target__';
  function CustomTooltip (line 14) | function CustomTooltip({ active, label, payload }) {
  class LongTermGoalGraph (line 41) | class LongTermGoalGraph extends React.Component {
    method constructor (line 42) | constructor(props) {
    method componentDidMount (line 47) | componentDidMount() {
    method fetchData (line 51) | async fetchData() {
    method render (line 110) | render() {

FILE: src/plugins/kaustubh/long_term_goals/LongTermGoalsSettings.js
  function addNewItem (line 10) | function addNewItem(items) {
  function renderRow (line 22) | function renderRow(props) {
  function LongTermGoalsSettings (line 67) | function LongTermGoalsSettings(props) {

FILE: src/plugins/kaustubh/long_term_goals/client.js
  method getSettingsKey (line 8) | static getSettingsKey() {
  method getSettingsComponent (line 12) | static getSettingsComponent(props) {
  method getDisplayLocation (line 16) | static getDisplayLocation() {
  method getTabData (line 20) | static getTabData() {
  method getDisplayComponent (line 27) | static getDisplayComponent(props) {

FILE: src/plugins/kaustubh/more_event_lists/MoreEventListsSettings.js
  function renderRow (line 27) | function renderRow(props) {
  function MoreEventListsSettings (line 46) | function MoreEventListsSettings(props) {

FILE: src/plugins/kaustubh/more_event_lists/client.js
  method getSettingsKey (line 7) | static getSettingsKey() {
  method getSettingsComponent (line 11) | static getSettingsComponent(props) {
  method getDisplayLocation (line 15) | static getDisplayLocation() {
  method getDisplayComponent (line 19) | static getDisplayComponent(props) {

FILE: src/plugins/kaustubh/time_sections/TimeSection.js
  class TimeSection (line 8) | class TimeSection extends React.Component {
    method constructor (line 9) | constructor(props) {
    method componentWillUnmount (line 17) | componentWillUnmount() {
    method render (line 22) | render() {

FILE: src/plugins/kaustubh/time_sections/TimeSectionSettings.js
  constant TIMEZONE_OPTIONS (line 11) | const TIMEZONE_OPTIONS = [{ label: '(timezone)', value: '' }].concat(Sel...
  function renderRow (line 13) | function renderRow(props) {
  function TimeSectionSettings (line 38) | function TimeSectionSettings(props) {

FILE: src/plugins/kaustubh/time_sections/client.js
  method getSettingsKey (line 8) | static getSettingsKey() {
  method getSettingsComponent (line 12) | static getSettingsComponent(props) {
  method getDisplayLocation (line 16) | static getDisplayLocation() {
  method getDisplayComponent (line 20) | static getDisplayComponent(props) {

FILE: src/plugins/kaustubh/topic_reminder_sections/TopicRemindersSection.js
  class TopicRemindersSection (line 8) | class TopicRemindersSection extends React.Component {
    method constructor (line 9) | constructor(props) {
    method componentDidMount (line 14) | componentDidMount() {
    method componentWillUnmount (line 28) | componentWillUnmount() {
    method renderContents (line 32) | renderContents() {
    method render (line 45) | render() {

FILE: src/plugins/kaustubh/topic_reminder_sections/TopicRemindersSectionSettings.js
  function renderRow (line 11) | function renderRow(props) {
  function TopicRemindersSettings (line 38) | function TopicRemindersSettings(props) {

FILE: src/plugins/kaustubh/topic_reminder_sections/client.js
  method getSettingsKey (line 8) | static getSettingsKey() {
  method getSettingsComponent (line 12) | static getSettingsComponent(props) {
  method getDisplayLocation (line 16) | static getDisplayLocation() {
  method getDisplayComponent (line 20) | static getDisplayComponent(props) {

FILE: src/plugins/kaustubh/topic_sections/TopicSection.js
  class TopicSection (line 7) | class TopicSection extends React.Component {
    method constructor (line 8) | constructor(props) {
    method componentDidMount (line 13) | componentDidMount() {
    method componentWillUnmount (line 23) | componentWillUnmount() {
    method renderContent (line 27) | renderContent() {
    method render (line 50) | render() {

FILE: src/plugins/kaustubh/topic_sections/TopicSectionSettings.js
  function renderRow (line 10) | function renderRow(props) {
  function TopicSectionSettings (line 30) | function TopicSectionSettings(props) {

FILE: src/plugins/kaustubh/topic_sections/client.js
  method getSettingsKey (line 8) | static getSettingsKey() {
  method getSettingsComponent (line 12) | static getSettingsComponent(props) {
  method getDisplayLocation (line 16) | static getDisplayLocation() {
  method getDisplayComponent (line 20) | static getDisplayComponent(props) {

FILE: src/server/__tests__/Config.test.js
  constant CONFIG_FORMAT (line 3) | const CONFIG_FORMAT = {
  function check (line 20) | function check(pattern, value) {
  function ensureValidConfig (line 49) | function ensureValidConfig(configPath) {

FILE: src/server/actions.js
  function readDirectory (line 16) | function readDirectory(directory) {
  function Module (line 28) | function Module(file) {
  class ActionsRegistry (line 37) | class ActionsRegistry {
    method get (line 38) | static get(configPlugins) {
    method build (line 66) | static build(result, nameToMethods) {
    method useCache (line 77) | static useCache(result, name, method) {
  method constructor (line 106) | constructor(config, database) {
  method registerBroadcast (line 115) | registerBroadcast(socket) {
  method getBroadcasts (line 119) | getBroadcasts() { // used for tests
  method has (line 126) | has(name) {
  method invoke (line 130) | async invoke(name, input, moreContext = {}) {

FILE: src/server/actions/__tests__/Backup.test.js
  function tmpDir (line 9) | function tmpDir() {

FILE: src/server/actions/__tests__/Reminders.test.js
  function checkIfReminderIsShown (line 6) | async function checkIfReminderIsShown(todayLabel, shown) {
  function checkReminderScore (line 79) | async function checkReminderScore(todayLabel, value, deadline) {

FILE: src/server/actions/__tests__/TestUtils.js
  function getBool (line 10) | function getBool(item, key, defaultValue) {
  class TestUtils (line 14) | class TestUtils {
    method beforeEach (line 15) | static async beforeEach() {
    method getActions (line 26) | static getActions() {
    method afterEach (line 30) | static async afterEach() {
    method loadData (line 34) | static async loadData(data) {

FILE: src/server/actions/backup.js
  function getDateAndTime (line 10) | function getDateAndTime() {
  function parseDateAndTime (line 21) | function parseDateAndTime(date, time) {
  function getFileName (line 30) | function getFileName({ date, time, hash }) {
  function parseFileName (line 34) | function parseFileName(filename) {

FILE: src/server/actions/database.js
  constant DATA_FORMAT_VERSION_KEY (line 50) | const DATA_FORMAT_VERSION_KEY = '__data_format_version__';

FILE: src/server/actions/reminders.js
  function getSuppressUntilDate (line 165) | function getSuppressUntilDate(logStructure, todayLabel) {

FILE: src/server/actions/settings.js
  constant INTERNAL_SETTINGS_PREFIX (line 7) | const INTERNAL_SETTINGS_PREFIX = '_';

FILE: src/server/database.js
  method constructor (line 10) | constructor(config) {
  method getTransaction (line 23) | getTransaction() {
  method reset (line 31) | async reset() {
  method close (line 48) | async close() {
  method getModelSequence (line 52) | getModelSequence() {
  method build (line 56) | async build(name) {
  method create (line 61) | async create(name, fields) {
  method update (line 72) | async update(name, fields) {
  method createOrUpdateItem (line 80) | async createOrUpdateItem(name, item, fields) {
  method findAll (line 88) | async findAll(name, where, order, limit) {
  method findOne (line 96) | async findOne(name, where, order) {
  method findByPk (line 102) | async findByPk(name, id) {
  method findItem (line 108) | async findItem(name, item) {
  method count (line 115) | async count(name, where, group) {
  method createOrFind (line 121) | async createOrFind(name, where, updateFields) {
  method deleteAll (line 131) | async deleteAll(name, where) {
  method deleteByPk (line 137) | async deleteByPk(name, id) {
  method getEdges (line 144) | async getEdges(edgeName, leftName, leftId) {
  method getNodesByEdge (line 157) | async getNodesByEdge(edgeName, leftName, leftId, rightName, rightType) {
  method setEdges (line 167) | async setEdges(edgeName, leftName, leftId, rightName, right) {

FILE: src/server/index.js
  function init (line 17) | async function init() {
  function startServer (line 22) | async function startServer() {
  function cleanup (line 40) | async function cleanup() {
  function main (line 49) | async function main(argv) {

FILE: src/server/models.js
  function getDataFormatVersion (line 3) | function getDataFormatVersion() {
  function getDataModels (line 11) | function getDataModels(sequelize) {
Condensed preview — 199 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (581K chars).
[
  {
    "path": ".gitignore",
    "chars": 46,
    "preview": ".DS_Store\n/config.json\ndata\ndist\nnode_modules\n"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 31,
    "preview": "yarn run lint && yarn run test\n"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) Kaustubh Karkare\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 4380,
    "preview": "## Generic Life Activity Data Organization System (GLADOS)\n\nhttps://user-images.githubusercontent.com/1102450/147822871-"
  },
  {
    "path": "config/babel.config.js",
    "chars": 271,
    "preview": "module.exports = {\n    plugins: [\n        '@babel/plugin-proposal-class-properties',\n        '@babel/plugin-transform-ru"
  },
  {
    "path": "config/demo.glados.json",
    "chars": 320,
    "preview": "{\n    \"lock_name\": \"glados-demo\",\n    \"database\": {\n        \"dialect\": \"sqlite\",\n        \"storage\": \"dist/demo/test.sqli"
  },
  {
    "path": "config/eslint.config.js",
    "chars": 1780,
    "preview": "module.exports = {\n    env: {\n        browser: true,\n        es6: true,\n        jest: true,\n        node: true,\n    },\n "
  },
  {
    "path": "config/example.glados.json",
    "chars": 278,
    "preview": "{\n    \"database\": {\n        \"dialect\": \"sqlite\",\n        \"storage\": \"data/test.sqlite\",\n        \"logging\": false\n    },\n"
  },
  {
    "path": "config/jest.config.js",
    "chars": 233,
    "preview": "const path = require('path');\n\nmodule.exports = {\n    rootDir: '..',\n    roots: ['src'],\n    testRegex: 'test.js',\n    t"
  },
  {
    "path": "config/webpack.config.js",
    "chars": 3967,
    "preview": "const webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst MiniCssExtractPlugin"
  },
  {
    "path": "package.json",
    "chars": 3353,
    "preview": "{\n  \"name\": \"glados\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Generic Life Activity Data Organization System\",\n  \"privat"
  },
  {
    "path": "src/README.md",
    "chars": 2385,
    "preview": "### Code Organization\n\n* While `server/` and `client/` are self explanatory, `common/` and `plugins/` contain code that "
  },
  {
    "path": "src/client/Application/Application.js",
    "chars": 10162,
    "preview": "import React from 'react';\nimport Col from 'react-bootstrap/Col';\nimport Container from 'react-bootstrap/Container';\nimp"
  },
  {
    "path": "src/client/Application/BackupSection.js",
    "chars": 1463,
    "preview": "import React from 'react';\n\nimport {\n    Coordinator, DataLoader, LeftRight, SidebarSection,\n} from '../Common';\n\nclass "
  },
  {
    "path": "src/client/Application/CreditsSection.js",
    "chars": 456,
    "preview": "import React from 'react';\n\nimport { SidebarSection } from '../Common';\n\nfunction CreditsSection(props) {\n    return (\n "
  },
  {
    "path": "src/client/Application/DetailsSection.css",
    "chars": 211,
    "preview": ".details-section .scrollable-section .text-editor {\n    background-color: var(--component-color);\n    padding: 4px;\n}\n\n."
  },
  {
    "path": "src/client/Application/DetailsSection.js",
    "chars": 9496,
    "preview": "import './DetailsSection.css';\n\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport InputGrou"
  },
  {
    "path": "src/client/Application/FavoritesSection.js",
    "chars": 1871,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { DataLoader, SidebarSection } from '../Common';\n"
  },
  {
    "path": "src/client/Application/IndexSection.css",
    "chars": 100,
    "preview": ".index-section {\n    margin-bottom: 128px;\n}\n\n.index-section .text-editor {\n    max-width: 500px;\n}\n"
  },
  {
    "path": "src/client/Application/IndexSection.js",
    "chars": 2524,
    "preview": "import React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { DateRangePicker, ScrollableSec"
  },
  {
    "path": "src/client/Application/TabSection.js",
    "chars": 2637,
    "preview": "import React from 'react';\n\nimport { Enum } from '../../common/data_types';\nimport { PluginDisplayLocation, SettingsCont"
  },
  {
    "path": "src/client/Application/URLState.js",
    "chars": 2957,
    "preview": "import { Coordinator, DateRangePicker, URLManager } from '../Common';\n\n/**\n * [...Array(128).keys()]\n *     .map(code =>"
  },
  {
    "path": "src/client/Application/index.js",
    "chars": 113,
    "preview": "// eslint-disable-next-line import/prefer-default-export\nexport { default as Application } from './Application';\n"
  },
  {
    "path": "src/client/Bootstrap/InputGroup.css",
    "chars": 1593,
    "preview": ".input-group:focus {\n    outline: none;\n}\n\n.input-group > * {\n    border-style: solid;\n    border-color: transparent;\n  "
  },
  {
    "path": "src/client/Bootstrap/Modal.css",
    "chars": 189,
    "preview": ".modal-dialog {\n    max-width: 800px;\n}\n\n.modal-content {\n    background-color: var(--background-color);\n    border-colo"
  },
  {
    "path": "src/client/Bootstrap/Popover.css",
    "chars": 123,
    "preview": ".popover {\n    max-width: none;\n}\n\n.popover-header,\n.popover-body {\n    background-color: var(--input-background-color);"
  },
  {
    "path": "src/client/Bootstrap/index.js",
    "chars": 120,
    "preview": "import 'bootstrap/dist/css/bootstrap.min.css';\nimport './InputGroup.css';\nimport './Modal.css';\nimport './Popover.css';\n"
  },
  {
    "path": "src/client/Common/AddLinkPlugin.js",
    "chars": 2357,
    "preview": "// https://bitwiser.in/2017/05/11/creating-rte-part-3-entities-and-decorators.html\n\n/* eslint-disable */\n\nimport React f"
  },
  {
    "path": "src/client/Common/AsyncSelector.js",
    "chars": 2280,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\n\nimport DataLoad"
  },
  {
    "path": "src/client/Common/BulletList/BulletList.css",
    "chars": 184,
    "preview": ".bullet-list .pager {\n    color: var(--text-disabled-color);\n}\n\n.bullet-list .pager > span {\n    cursor: pointer;\n}\n\n.bu"
  },
  {
    "path": "src/client/Common/BulletList/BulletList.js",
    "chars": 8237,
    "preview": "import './BulletList.css';\n\nimport arrayMove from 'array-move';\nimport classNames from 'classnames';\nimport deepEqual fr"
  },
  {
    "path": "src/client/Common/BulletList/BulletListIcon.js",
    "chars": 734,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { KeyCodes } from '../Utils';\n\nfunction BulletLis"
  },
  {
    "path": "src/client/Common/BulletList/BulletListItem.js",
    "chars": 8351,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimpo"
  },
  {
    "path": "src/client/Common/BulletList/BulletListLine.js",
    "chars": 524,
    "preview": "import React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { GoPrimitiveDot } from 'react-ic"
  },
  {
    "path": "src/client/Common/BulletList/BulletListPager.js",
    "chars": 2853,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport Highlightable from '../Highlightable';\nimport { K"
  },
  {
    "path": "src/client/Common/BulletList/BulletListTitle.js",
    "chars": 2955,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimpo"
  },
  {
    "path": "src/client/Common/BulletList/index.js",
    "chars": 40,
    "preview": "export { default } from './BulletList';\n"
  },
  {
    "path": "src/client/Common/ConfirmModal.js",
    "chars": 1310,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport Modal"
  },
  {
    "path": "src/client/Common/Coordinator.js",
    "chars": 802,
    "preview": "const callbacks = {};\n\nclass Coordinator {\n    static register(name, callback) {\n        callbacks[name] = callback;\n   "
  },
  {
    "path": "src/client/Common/DataLoader.js",
    "chars": 2852,
    "preview": "import deepEqual from 'deep-equal';\nimport deepcopy from 'deepcopy';\n\nimport { getPartialItem, isItem } from '../../comm"
  },
  {
    "path": "src/client/Common/DateContext.js",
    "chars": 290,
    "preview": "import React from 'react';\n\nconst DateContext = React.createContext(null);\n\nDateContext.Wrapper = (Component) => (morePr"
  },
  {
    "path": "src/client/Common/DatePicker.js",
    "chars": 1382,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport { Calendar } from 'react-date-range';\n\nimport Date"
  },
  {
    "path": "src/client/Common/DateRangePicker.js",
    "chars": 3172,
    "preview": "// https://adphorus.github.io/react-date-range/\nimport 'react-date-range/dist/styles.css'; // main css file\nimport 'reac"
  },
  {
    "path": "src/client/Common/Dropdown.css",
    "chars": 324,
    "preview": ".dropdown-toggle::after {\n    display: none;\n}\n\n/**\n * There are multiple reports of this problem.\n * https://stackoverf"
  },
  {
    "path": "src/client/Common/Dropdown.js",
    "chars": 2994,
    "preview": "import './Dropdown.css';\n\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport Dropdown from 'react-boo"
  },
  {
    "path": "src/client/Common/EditorModal.js",
    "chars": 4980,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport Input"
  },
  {
    "path": "src/client/Common/EnumSelectorSection.js",
    "chars": 1334,
    "preview": "import React from 'react';\n\nimport PropTypes from '../prop-types';\nimport LeftRight from './LeftRight';\nimport SidebarSe"
  },
  {
    "path": "src/client/Common/ErrorModal.js",
    "chars": 895,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Modal from 'react-bootstrap/Modal';\n\nimport { supp"
  },
  {
    "path": "src/client/Common/Highlightable.css",
    "chars": 127,
    "preview": "\n.highlightable:focus {\n    outline: none;\n}\n\n.highlightable.highlighted {\n    background: var(--component-highlight-col"
  },
  {
    "path": "src/client/Common/Highlightable.js",
    "chars": 1840,
    "preview": "import './Highlightable.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React fro"
  },
  {
    "path": "src/client/Common/Icon.css",
    "chars": 269,
    "preview": ".icon {\n    cursor: pointer;\n    height: 20px;\n    position: relative;\n    top: -1px;\n    width: 13px;\n}\n\n.icon > svg {\n"
  },
  {
    "path": "src/client/Common/Icon.js",
    "chars": 756,
    "preview": "import './Icon.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'react'"
  },
  {
    "path": "src/client/Common/InfoModal.js",
    "chars": 792,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Modal from 'react-bootstrap/Modal';\n\nimport { supp"
  },
  {
    "path": "src/client/Common/InputLine.css",
    "chars": 218,
    "preview": ".input-line {\n    flex: 1 1 auto;\n    height: auto;\n    overflow: hidden;\n    width: 0px;\n}\n\n.input-line.overflow {\n    "
  },
  {
    "path": "src/client/Common/InputLine.js",
    "chars": 683,
    "preview": "import './InputLine.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'r"
  },
  {
    "path": "src/client/Common/LeftRight.js",
    "chars": 354,
    "preview": "/* eslint-disable react/prop-types */\n\nimport React from 'react';\n\nfunction LeftRight(props) {\n    return (\n        <div"
  },
  {
    "path": "src/client/Common/Link.js",
    "chars": 1062,
    "preview": "import assert from 'assert';\nimport React from 'react';\n\nimport PropTypes from '../prop-types';\nimport Coordinator from "
  },
  {
    "path": "src/client/Common/ModalStack.js",
    "chars": 2399,
    "preview": "import assert from 'assert';\nimport React from 'react';\n\nimport ConfirmModal from './ConfirmModal';\nimport Coordinator f"
  },
  {
    "path": "src/client/Common/Plugins.js",
    "chars": 2537,
    "preview": "/* eslint-disable max-classes-per-file */\n\nimport React from 'react';\n\nimport { Enum } from '../../common/data_types';\ni"
  },
  {
    "path": "src/client/Common/PopoverElement.js",
    "chars": 1368,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport Overl"
  },
  {
    "path": "src/client/Common/ScrollableSection.css",
    "chars": 367,
    "preview": ".scrollable-section {\n    padding-right: 4px;\n    overflow-y: scroll;\n}\n\n.scrollable-section::-webkit-scrollbar {\n    wi"
  },
  {
    "path": "src/client/Common/ScrollableSection.js",
    "chars": 1547,
    "preview": "/* eslint-disable max-classes-per-file */\n\nimport './ScrollableSection.css';\n\nimport PropTypes from 'prop-types';\nimport"
  },
  {
    "path": "src/client/Common/Selector.js",
    "chars": 2423,
    "preview": "/* eslint-disable max-classes-per-file */\n\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport Form fr"
  },
  {
    "path": "src/client/Common/SettingsContext.js",
    "chars": 308,
    "preview": "import React from 'react';\n\nconst SettingsContext = React.createContext({});\n\nSettingsContext.Wrapper = (Component) => ("
  },
  {
    "path": "src/client/Common/SidebarSection.css",
    "chars": 728,
    "preview": ".sidebar-section {\n    border: 1px solid var(--background-color);\n    border-radius: 4px;\n    padding: 3px 8px 5px;\n    "
  },
  {
    "path": "src/client/Common/SidebarSection.js",
    "chars": 2429,
    "preview": "import './SidebarSection.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React fr"
  },
  {
    "path": "src/client/Common/SortableList.css",
    "chars": 116,
    "preview": "button.sortableDragHandle.btn {\n    cursor: grab;\n}\n\n.sortableDraggedItem {\n    cursor: grab;\n    z-index: 10000;\n}\n"
  },
  {
    "path": "src/client/Common/SortableList.js",
    "chars": 3425,
    "preview": "import './SortableList.css';\n\nimport arrayMove from 'array-move';\nimport PropTypes from 'prop-types';\nimport React from "
  },
  {
    "path": "src/client/Common/StandardIcons.js",
    "chars": 1019,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport { AiOutlineWarning } from 'react-icons/ai';\nimport"
  },
  {
    "path": "src/client/Common/TextEditor.css",
    "chars": 1524,
    "preview": ".public-DraftStyleDefault-ul,\n.public-DraftStyleDefault-ol {\n    margin: 0;\n}\n\n.text-editor {\n    position: relative;\n}\n"
  },
  {
    "path": "src/client/Common/TextEditor.js",
    "chars": 10972,
    "preview": "import 'draft-js/dist/Draft.css';\nimport './TextEditor.css';\n\nimport assert from 'assert';\nimport classNames from 'class"
  },
  {
    "path": "src/client/Common/TextInput.js",
    "chars": 850,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\n\nclass TextInput"
  },
  {
    "path": "src/client/Common/TooltipElement.js",
    "chars": 689,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport OverlayTrigger from 'react-bootstrap/OverlayTrigge"
  },
  {
    "path": "src/client/Common/TypeaheadInput.js",
    "chars": 1946,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport { AsyncTypeahead } from 'react-bootstrap-typeahead"
  },
  {
    "path": "src/client/Common/TypeaheadOptions.js",
    "chars": 4215,
    "preview": "import assert from 'assert';\n\nclass TypeaheadOptions {\n    static getFromTypes(names) {\n        return new TypeaheadOpti"
  },
  {
    "path": "src/client/Common/TypeaheadSelector.css",
    "chars": 618,
    "preview": ".rbt input:first-child {\n    background-color: var(--input-background-color);\n    color: var(--input-text-color);\n    fo"
  },
  {
    "path": "src/client/Common/TypeaheadSelector.js",
    "chars": 4589,
    "preview": "import 'react-bootstrap-typeahead/css/Typeahead.min.css';\nimport './TypeaheadSelector.css';\n\nimport PropTypes from 'prop"
  },
  {
    "path": "src/client/Common/URLManager.js",
    "chars": 935,
    "preview": "import assert from 'assert';\nimport queryString from 'query-string';\n\nlet onChange;\nlet pushState;\n\nconst options = { ar"
  },
  {
    "path": "src/client/Common/Utils.js",
    "chars": 737,
    "preview": "export function suppressUnlessShiftKey(event) {\n    if (!event.shiftKey) {\n        event.preventDefault();\n    }\n}\n\n// h"
  },
  {
    "path": "src/client/Common/index.js",
    "chars": 1790,
    "preview": "export { default as AsyncSelector } from './AsyncSelector';\nexport { default as BulletList } from './BulletList';\nexport"
  },
  {
    "path": "src/client/Graphs/GraphLineChart.js",
    "chars": 1609,
    "preview": "import React from 'react';\nimport {\n    CartesianGrid, Legend, Line, LineChart, ResponsiveContainer,\n    Tooltip, XAxis,"
  },
  {
    "path": "src/client/Graphs/GraphSection.css",
    "chars": 134,
    "preview": ".graph-tooltip {\n    background-color: var(--component-color);\n    padding: 4px;\n    border-radius: 4px;\n    white-space"
  },
  {
    "path": "src/client/Graphs/GraphSection.js",
    "chars": 3634,
    "preview": "import './GraphSection.css';\n\nimport deepEqual from 'deep-equal';\nimport React from 'react';\n\nimport { DataLoader } from"
  },
  {
    "path": "src/client/Graphs/GraphSectionData.js",
    "chars": 4640,
    "preview": "import { addDays, compareAsc } from 'date-fns';\n\nimport { LogKey } from '../../common/data_types';\nimport DateUtils from"
  },
  {
    "path": "src/client/Graphs/GraphSectionOptions.js",
    "chars": 1751,
    "preview": "import {\n    getDay, getMonth, getYear, subDays,\n} from 'date-fns';\n\nimport { Enum } from '../../common/data_types';\nimp"
  },
  {
    "path": "src/client/Graphs/GraphTooltip.js",
    "chars": 910,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nfunction NormalTooltip({ active, label, payload }) {\n   "
  },
  {
    "path": "src/client/Graphs/index.js",
    "chars": 224,
    "preview": "export { default as GraphSection } from './GraphSection';\nexport { default as GraphLineChart } from './GraphLineChart';\n"
  },
  {
    "path": "src/client/LogEvent/LogEventAdder.js",
    "chars": 3110,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { isRealItem, LogEvent } from '../../common/data_"
  },
  {
    "path": "src/client/LogEvent/LogEventDetailsHeader.js",
    "chars": 714,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    InputLine, TextEditor,\n} from '../Common';\n"
  },
  {
    "path": "src/client/LogEvent/LogEventEditor.js",
    "chars": 7411,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/client/LogEvent/LogEventList.js",
    "chars": 2822,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { BulletList, DetailsIcon, TextEditor } from '../"
  },
  {
    "path": "src/client/LogEvent/LogEventOptions.js",
    "chars": 2949,
    "preview": "import assert from 'assert';\n\nimport { getVirtualID } from '../../common/data_types';\nimport { TypeaheadOptions } from '"
  },
  {
    "path": "src/client/LogEvent/LogEventSearch.js",
    "chars": 10460,
    "preview": "import assert from 'assert';\nimport { addDays, eachDayOfInterval, getDay } from 'date-fns';\nimport React from 'react';\n\n"
  },
  {
    "path": "src/client/LogEvent/index.js",
    "chars": 322,
    "preview": "export { default as LogEventEditor } from './LogEventEditor';\nexport { default as LogEventList } from './LogEventList';\n"
  },
  {
    "path": "src/client/LogKey/LogKeyEditor.js",
    "chars": 6001,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/client/LogKey/LogKeyListEditor.js",
    "chars": 2618,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport Input"
  },
  {
    "path": "src/client/LogKey/LogValueEditor.js",
    "chars": 4331,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { getPartialItem, LogKey } from '../../common/dat"
  },
  {
    "path": "src/client/LogKey/LogValueListEditor.js",
    "chars": 1622,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/client/LogKey/index.js",
    "chars": 256,
    "preview": "export { default as LogKeyEditor } from './LogKeyEditor';\nexport { default as LogKeyListEditor } from './LogKeyListEdito"
  },
  {
    "path": "src/client/LogStructure/LogStructureDetailsHeader.js",
    "chars": 743,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    Coordinator, InputLine,\n} from '../Common';"
  },
  {
    "path": "src/client/LogStructure/LogStructureEditor.js",
    "chars": 6962,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/client/LogStructure/LogStructureFrequencyEditor.js",
    "chars": 7640,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/client/LogStructure/LogStructureGroupEditor.js",
    "chars": 1560,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/client/LogStructure/LogStructureGroupList.js",
    "chars": 1322,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { BulletList } from '../Common';\nimport LogStruct"
  },
  {
    "path": "src/client/LogStructure/LogStructureList.js",
    "chars": 2363,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    BulletList, DetailsIcon, InfoIcon, LeftRigh"
  },
  {
    "path": "src/client/LogStructure/LogStructureOptions.js",
    "chars": 1310,
    "preview": "import assert from 'assert';\n\nimport { TypeaheadOptions } from '../Common';\n\nclass LogStructureOptions {\n    static get("
  },
  {
    "path": "src/client/LogStructure/LogStructureSearch.js",
    "chars": 1544,
    "preview": "import React from 'react';\n\nimport PropTypes from '../prop-types';\nimport LogStructureGroupList from './LogStructureGrou"
  },
  {
    "path": "src/client/LogStructure/index.js",
    "chars": 366,
    "preview": "export { default as LogStructureEditor } from './LogStructureEditor';\nexport { default as LogStructureGroupList } from '"
  },
  {
    "path": "src/client/LogTopic/LogTopicDetailsHeader.js",
    "chars": 1824,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    Coordinator, Dropdown, InputLine, Link,\n} f"
  },
  {
    "path": "src/client/LogTopic/LogTopicEditor.js",
    "chars": 6096,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/client/LogTopic/LogTopicList.js",
    "chars": 1988,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    BulletList, DetailsIcon, Link, WarningIcon,"
  },
  {
    "path": "src/client/LogTopic/LogTopicOptions.js",
    "chars": 2614,
    "preview": "import assert from 'assert';\n\nimport { getVirtualID, isRealItem, LogTopic } from '../../common/data_types';\nimport { Coo"
  },
  {
    "path": "src/client/LogTopic/LogTopicSearch.js",
    "chars": 1871,
    "preview": "import React from 'react';\n\nimport { TypeaheadOptions } from '../Common';\nimport PropTypes from '../prop-types';\nimport "
  },
  {
    "path": "src/client/LogTopic/index.js",
    "chars": 322,
    "preview": "export { default as LogTopicList } from './LogTopicList';\nexport { default as LogTopicEditor } from './LogTopicEditor';\n"
  },
  {
    "path": "src/client/Reminders/ReminderItem.js",
    "chars": 5289,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport InputGrou"
  },
  {
    "path": "src/client/Reminders/ReminderList.js",
    "chars": 922,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { SidebarSection } from '../Common';\nimport Remin"
  },
  {
    "path": "src/client/Reminders/ReminderSidebar.js",
    "chars": 1404,
    "preview": "import React from 'react';\n\nimport { DataLoader, DateContext } from '../Common';\nimport PropTypes from '../prop-types';\n"
  },
  {
    "path": "src/client/Reminders/index.js",
    "chars": 121,
    "preview": "// eslint-disable-next-line import/prefer-default-export\nexport { default as ReminderSidebar } from './ReminderSidebar';"
  },
  {
    "path": "src/client/Settings/SettingsEditor.js",
    "chars": 4005,
    "preview": "import assert from 'assert';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport {\n "
  },
  {
    "path": "src/client/Settings/SettingsModal.js",
    "chars": 2472,
    "preview": "import React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport InputGroup from 'react-bootstrap/InputGro"
  },
  {
    "path": "src/client/Settings/SettingsSection.js",
    "chars": 1681,
    "preview": "import React from 'react';\n\nimport { LeftRight, SettingsContext, SidebarSection } from '../Common';\nimport PropTypes fro"
  },
  {
    "path": "src/client/Settings/index.js",
    "chars": 121,
    "preview": "// eslint-disable-next-line import/prefer-default-export\nexport { default as SettingsSection } from './SettingsSection';"
  },
  {
    "path": "src/client/__tests__/Colors.test.js",
    "chars": 670,
    "preview": "const fs = require('fs');\nconst path = require('path');\nconst walkSync = require('walk-sync');\n\ntest('verify_no_random_c"
  },
  {
    "path": "src/client/index.css",
    "chars": 1076,
    "preview": "body {\n    --background-color: #111;\n    --component-color: #222;\n    --component-highlight-color: #333;\n    --text-colo"
  },
  {
    "path": "src/client/index.html",
    "chars": 137,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>GLADOS</title>\n  </head>\n  <body onLoad=\"main()\">\n    <div id=\"root\"></div>\n "
  },
  {
    "path": "src/client/index.js",
    "chars": 2003,
    "preview": "import './Bootstrap';\nimport './index.css';\nimport './prop-types'; // Load PropTypes.Custom\n\nimport React from 'react';\n"
  },
  {
    "path": "src/client/prop-types.js",
    "chars": 1563,
    "preview": "import PropTypes from 'prop-types';\n\nconst DateRange = PropTypes.shape({\n    startDate: PropTypes.string.isRequired,\n   "
  },
  {
    "path": "src/common/AsyncUtils.js",
    "chars": 1500,
    "preview": "export function asyncSequence(items, method) {\n    if (!items) {\n        return Promise.resolve();\n    }\n    return new "
  },
  {
    "path": "src/common/DateUtils.js",
    "chars": 2150,
    "preview": "import assert from 'assert';\nimport {\n    addDays, format, isValid, parse, set, subDays,\n} from 'date-fns';\n\nconst MS_IN"
  },
  {
    "path": "src/common/RichTextUtils.js",
    "chars": 21425,
    "preview": "import assert from 'assert';\nimport deepEqual from 'deep-equal';\nimport {\n    convertFromRaw, convertToRaw,\n    EditorSt"
  },
  {
    "path": "src/common/SocketRPC.js",
    "chars": 5333,
    "preview": "import assert from 'assert';\n\nconst SERVER_SIDE = 'server_side';\nconst CLIENT_SIDE = 'client_side';\n\nconst GENERAL_REQUE"
  },
  {
    "path": "src/common/__tests__/RichTextUtils.test.js",
    "chars": 2345,
    "preview": "import RichTextUtils from '../RichTextUtils';\n\nconst { StorageType } = RichTextUtils;\n\nconst typeToValue = {\n    [Storag"
  },
  {
    "path": "src/common/data_types/LogEvent.js",
    "chars": 11349,
    "preview": "import assert from 'assert';\n\nimport RichTextUtils from '../RichTextUtils';\nimport DataTypeBase from './base';\nimport Lo"
  },
  {
    "path": "src/common/data_types/LogKey.js",
    "chars": 7082,
    "preview": "import RichTextUtils from '../RichTextUtils';\nimport Enum from './enum';\nimport { getPartialItem, getVirtualID } from '."
  },
  {
    "path": "src/common/data_types/LogStructure.js",
    "chars": 14289,
    "preview": "import { asyncSequence } from '../AsyncUtils';\nimport RichTextUtils from '../RichTextUtils';\nimport DataTypeBase from '."
  },
  {
    "path": "src/common/data_types/LogStructureFrequency.js",
    "chars": 2907,
    "preview": "import {\n    addDays, addYears,\n    compareAsc,\n    getDay,\n    isFriday, isMonday, isSaturday, isSunday,\n    setDate, s"
  },
  {
    "path": "src/common/data_types/LogStructureGroup.js",
    "chars": 2511,
    "preview": "import DataTypeBase from './base';\nimport { getVirtualID } from './utils';\nimport { validateNonEmptyString } from './val"
  },
  {
    "path": "src/common/data_types/LogTopic.js",
    "chars": 15407,
    "preview": "import { asyncSequence } from '../AsyncUtils';\nimport RichTextUtils from '../RichTextUtils';\nimport DataTypeBase from '."
  },
  {
    "path": "src/common/data_types/__tests__/LogStructureFrequency.test.js",
    "chars": 1918,
    "preview": "import { addDays, compareAsc } from 'date-fns';\n\nimport DateUtils from '../../DateUtils';\nimport LogStructureFrequency f"
  },
  {
    "path": "src/common/data_types/api.js",
    "chars": 1434,
    "preview": "export default class DataTypeAPI {\n    // The process of adding a new data type should be consistent.\n\n    static create"
  },
  {
    "path": "src/common/data_types/base.js",
    "chars": 6304,
    "preview": "import assert from 'assert';\n\nimport { asyncSequence } from '../AsyncUtils';\nimport DataTypeAPI from './api';\nimport { i"
  },
  {
    "path": "src/common/data_types/enum.js",
    "chars": 368,
    "preview": "import assert from 'assert';\n\nexport default function Enum(Options) {\n    const result = { Options };\n    Options.forEac"
  },
  {
    "path": "src/common/data_types/index.js",
    "chars": 578,
    "preview": "import LogEvent from './LogEvent';\nimport LogKey from './LogKey';\nimport LogStructure from './LogStructure';\nimport LogS"
  },
  {
    "path": "src/common/data_types/utils.js",
    "chars": 677,
    "preview": "let virtualID = 0;\n\nexport function getVirtualID() {\n    virtualID -= 1;\n    return virtualID;\n}\n\nexport function isItem"
  },
  {
    "path": "src/common/data_types/validation.js",
    "chars": 1816,
    "preview": "// A collection utilities used in the validate() methods of different data types.\n\nexport function validateNonEmptyStrin"
  },
  {
    "path": "src/common/polyfill.js",
    "chars": 519,
    "preview": "function getTimePrefix() {\n    const now = new Date();\n    return `[${now.toLocaleDateString()} ${now.toLocaleTimeString"
  },
  {
    "path": "src/demo/components/Application.js",
    "chars": 3489,
    "preview": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport DetailsSection from './Details"
  },
  {
    "path": "src/demo/components/BaseWrapper.js",
    "chars": 2839,
    "preview": "import assert from 'assert';\nimport { By, Key } from 'selenium-webdriver';\n\nimport { asyncSequence } from '../../common/"
  },
  {
    "path": "src/demo/components/BulletList.js",
    "chars": 2909,
    "preview": "/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\nimport { By } from 'selenium-webdriver';\n\nimport"
  },
  {
    "path": "src/demo/components/DetailsSection.js",
    "chars": 894,
    "preview": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport { TextEditor } from './Inputs'"
  },
  {
    "path": "src/demo/components/IndexSection.js",
    "chars": 1026,
    "preview": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport BulletList from './BulletList'"
  },
  {
    "path": "src/demo/components/Inputs.js",
    "chars": 5022,
    "preview": "/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\nimport { By } from 'selenium-webdriver';\n\nimport"
  },
  {
    "path": "src/demo/components/ModalDialog.js",
    "chars": 3109,
    "preview": "import assert from 'assert';\nimport { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport {"
  },
  {
    "path": "src/demo/components/ReminderItem.js",
    "chars": 910,
    "preview": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\n\nexport default class ReminderItem ex"
  },
  {
    "path": "src/demo/components/SidebarSection.js",
    "chars": 1032,
    "preview": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport ReminderItem from './ReminderI"
  },
  {
    "path": "src/demo/components/index.js",
    "chars": 70,
    "preview": "import Application from './Application';\n\nexport default Application;\n"
  },
  {
    "path": "src/demo/index.js",
    "chars": 4725,
    "preview": "/* eslint-disable no-console */\n\nimport { assert } from 'console';\nimport fs from 'fs';\nimport path from 'path';\nimport "
  },
  {
    "path": "src/demo/lessons/001-events.js",
    "chars": 4020,
    "preview": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexS"
  },
  {
    "path": "src/demo/lessons/002-topics.js",
    "chars": 5186,
    "preview": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexS"
  },
  {
    "path": "src/demo/lessons/003-structures.js",
    "chars": 4909,
    "preview": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexS"
  },
  {
    "path": "src/demo/lessons/004-reminders.js",
    "chars": 3706,
    "preview": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexS"
  },
  {
    "path": "src/demo/lessons/005-graphs.js",
    "chars": 7212,
    "preview": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexS"
  },
  {
    "path": "src/demo/lessons.js",
    "chars": 1326,
    "preview": "/* eslint-disable no-console */\n\nimport { asyncSequence } from '../common/AsyncUtils';\nimport Application from './compon"
  },
  {
    "path": "src/demo/process.js",
    "chars": 3538,
    "preview": "/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\nimport childProcess from 'child_process';\n\nclass"
  },
  {
    "path": "src/plugins/README.md",
    "chars": 615,
    "preview": "### Expectations\n\n* File names ending with `actions.js` are supposed to export an object, whose keys are \"action names\" "
  },
  {
    "path": "src/plugins/kaustubh/custom.actions.js",
    "chars": 13281,
    "preview": "/* eslint-disable func-names */\n/* eslint-disable camelcase */\n/* eslint-disable max-len */\n/* eslint-disable no-console"
  },
  {
    "path": "src/plugins/kaustubh/long_term_goals/LongTermGoalGraph.js",
    "chars": 4645,
    "preview": "import {\n    addDays, compareAsc, differenceInDays,\n} from 'date-fns';\nimport React from 'react';\n\nimport { DateContext "
  },
  {
    "path": "src/plugins/kaustubh/long_term_goals/LongTermGoalsSettings.js",
    "chars": 3167,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/plugins/kaustubh/long_term_goals/client.js",
    "chars": 889,
    "preview": "import React from 'react';\n\nimport { PluginClient, PluginDisplayLocation } from '../../../client/Common';\nimport LongTer"
  },
  {
    "path": "src/plugins/kaustubh/more_event_lists/MoreEventListsSettings.js",
    "chars": 2110,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/plugins/kaustubh/more_event_lists/client.js",
    "chars": 496,
    "preview": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport MoreEventListsSettings from '."
  },
  {
    "path": "src/plugins/kaustubh/time_sections/TimeSection.js",
    "chars": 1164,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { LeftRight, SidebarSection } from '../../../clie"
  },
  {
    "path": "src/plugins/kaustubh/time_sections/TimeSectionSettings.js",
    "chars": 2302,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimpo"
  },
  {
    "path": "src/plugins/kaustubh/time_sections/client.js",
    "chars": 773,
    "preview": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport TimeSection from './TimeSectio"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/TopicRemindersSection.js",
    "chars": 1854,
    "preview": "import React from 'react';\n\nimport {\n    DataLoader, DateContext, Link, SidebarSection,\n} from '../../../client/Common';"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/TopicRemindersSectionSettings.js",
    "chars": 3034,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/actions.js",
    "chars": 1574,
    "preview": "/* eslint-disable func-names */\n\nimport { differenceInCalendarDays } from 'date-fns';\n\nimport DateUtils from '../../../c"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/client.js",
    "chars": 894,
    "preview": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport TopicRemindersSection from './"
  },
  {
    "path": "src/plugins/kaustubh/topic_sections/TopicSection.js",
    "chars": 1781,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { DataLoader, SidebarSection } from '../../../cli"
  },
  {
    "path": "src/plugins/kaustubh/topic_sections/TopicSectionSettings.js",
    "chars": 2372,
    "preview": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimp"
  },
  {
    "path": "src/plugins/kaustubh/topic_sections/client.js",
    "chars": 757,
    "preview": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport TopicSection from './TopicSect"
  },
  {
    "path": "src/server/__tests__/Config.test.js",
    "chars": 1732,
    "preview": "import fs from 'fs';\n\nconst CONFIG_FORMAT = {\n    '?lock': 'string',\n    database: {\n        dialect: 'string',\n        "
  },
  {
    "path": "src/server/actions/__tests__/Backup.test.js",
    "chars": 1575,
    "preview": "import tmp from 'tmp';\n\nimport { LogTopic } from '../../../common/data_types';\nimport TestUtils from './TestUtils';\n\nbef"
  },
  {
    "path": "src/server/actions/__tests__/Database.test.js",
    "chars": 1345,
    "preview": "import TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\ntest('test_load"
  },
  {
    "path": "src/server/actions/__tests__/LogEvent.test.js",
    "chars": 3147,
    "preview": "import TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\ntest('test_stru"
  },
  {
    "path": "src/server/actions/__tests__/LogStructure.test.js",
    "chars": 6882,
    "preview": "import { LogKey } from '../../../common/data_types';\nimport RichTextUtils from '../../../common/RichTextUtils';\nimport T"
  },
  {
    "path": "src/server/actions/__tests__/LogTopic.test.js",
    "chars": 4966,
    "preview": "import { asyncSequence } from '../../../common/AsyncUtils';\nimport { LogKey } from '../../../common/data_types';\nimport "
  },
  {
    "path": "src/server/actions/__tests__/Reminders.test.js",
    "chars": 4350,
    "preview": "import TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\nasync function "
  },
  {
    "path": "src/server/actions/__tests__/TestUtils.js",
    "chars": 6984,
    "preview": "import { asyncSequence } from '../../../common/AsyncUtils';\nimport { getVirtualID, LogKey } from '../../../common/data_t"
  },
  {
    "path": "src/server/actions/backup.js",
    "chars": 4364,
    "preview": "/* eslint-disable func-names */\n\nimport assert from 'assert';\nimport crypto from 'crypto';\nimport fs from 'fs';\nimport p"
  },
  {
    "path": "src/server/actions/data_types.js",
    "chars": 2825,
    "preview": "/* eslint-disable func-names */\n\nimport { getDataTypeMapping } from '../../common/data_types';\n\nconst ActionsRegistry = "
  },
  {
    "path": "src/server/actions/database.js",
    "chars": 4300,
    "preview": "/* eslint-disable func-names */\n\nimport assert from 'assert';\nimport toposort from 'toposort';\n\nimport { asyncSequence }"
  },
  {
    "path": "src/server/actions/reminders.js",
    "chars": 7980,
    "preview": "/* eslint-disable func-names */\n\nimport assert from 'assert';\nimport { addDays, compareAsc, subDays } from 'date-fns';\n\n"
  },
  {
    "path": "src/server/actions/settings.js",
    "chars": 1225,
    "preview": "/* eslint-disable func-names */\n\nimport assert from 'assert';\n\nconst ActionsRegistry = {};\n\nconst INTERNAL_SETTINGS_PREF"
  },
  {
    "path": "src/server/actions/suggestions.js",
    "chars": 3172,
    "preview": "/* eslint-disable func-names */\n\nimport assert from 'assert';\n\nimport { LogKey } from '../../common/data_types';\n\nconst "
  },
  {
    "path": "src/server/actions.js",
    "chars": 7052,
    "preview": "/* eslint-disable func-names */\n/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\n\n// This method "
  },
  {
    "path": "src/server/database.js",
    "chars": 7189,
    "preview": "import assert from 'assert';\nimport fs from 'fs';\n\nimport { isRealItem } from '../common/data_types';\nimport { getDataMo"
  },
  {
    "path": "src/server/index.js",
    "chars": 2559,
    "preview": "/* eslint-disable no-console */\n\nimport '../common/polyfill';\n\nimport express from 'express';\nimport fs from 'fs';\nimpor"
  },
  {
    "path": "src/server/models.js",
    "chars": 11160,
    "preview": "const Sequelize = require('sequelize');\n\nexport function getDataFormatVersion() {\n    // This value is used to ensure th"
  }
]

About this extraction

This page contains the full source code of the kaustubh-karkare/glados GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 199 files (537.6 KB), approximately 120.2k tokens, and a symbol index with 841 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!