[
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/config.json\ndata\ndist\nnode_modules\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "yarn run lint && yarn run test\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Kaustubh Karkare\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "## Generic Life Activity Data Organization System (GLADOS)\n\nhttps://user-images.githubusercontent.com/1102450/147822871-ca69bedc-ed20-45aa-88de-e02c66bb92f0.mp4\n\nIf the above video does not work, you can also watch it here: https://www.youtube.com/watch?v=xd3JJi8zSk4\n\n### Rationale\n* 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!\n* 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.\n* 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.\n* 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! :)\n\n### Warning!\n\n* 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.\n\n### Installation\n\n```\ngit clone https://github.com/kaustubh-karkare/glados\ncd glados\ncp config/example.glados.json config.json\nmkdir data\nyarn install\nyarn run build\nyarn run database-reset\n```\n\n* The default `config.json` file specifies the `data` subdirectory as the location of the SQLite database and the backups.\n* I personally made `data` a symlink to another directory that synced to my [Dropbox](https://www.dropbox.com/).\n* You can theoretically change the config to use whatever storage you want, as long as it is compatible with [Sequelize](https://sequelize.org/).\n* And once you're ready,\n\n```\nyarn run server\n```\n\n### Demo\n\n* 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.\n* 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.\n\n```\nyarn run demo\n```\n\n* You can see the result at the top of this README file.\n* An auxiliary benefit here is that this functionality can be used as an E2E test for the client code.\n\n### Backups\n\n```\nyarn run backup-save  # Can also be done via the right-sidebar in the UI.\nyarn run backup-load  # This involves a database reset, so be careful!\n```\n\n* 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).\n* 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.\n* These are also useful if data needs to be moved from one storage to another.\n\n### Community\n\n* https://www.reddit.com/r/glados_app/\n* Hacker News: [2022-01-01](https://news.ycombinator.com/item?id=29756591).\n"
  },
  {
    "path": "config/babel.config.js",
    "content": "module.exports = {\n    plugins: [\n        '@babel/plugin-proposal-class-properties',\n        '@babel/plugin-transform-runtime',\n    ],\n    presets: [\n        '@babel/preset-env',\n        '@babel/preset-react',\n    ],\n    compact: false,\n    sourceType: 'unambiguous',\n};\n"
  },
  {
    "path": "config/demo.glados.json",
    "content": "{\n    \"lock_name\": \"glados-demo\",\n    \"database\": {\n        \"dialect\": \"sqlite\",\n        \"storage\": \"dist/demo/test.sqlite\",\n        \"logging\": false\n    },\n    \"backup\": {\n        \"location\": \"dist/demo\",\n        \"save_interval_ms\": null\n    },\n    \"server\": {\n        \"host\": \"localhost\",\n        \"port\": 8081\n    }\n}\n"
  },
  {
    "path": "config/eslint.config.js",
    "content": "module.exports = {\n    env: {\n        browser: true,\n        es6: true,\n        jest: true,\n        node: true,\n    },\n    extends: [\n        'plugin:react/recommended',\n        'airbnb',\n    ],\n    globals: {\n        Atomics: 'readonly',\n        SharedArrayBuffer: 'readonly',\n    },\n    parser: '@typescript-eslint/parser',\n    parserOptions: {\n        ecmaFeatures: {\n            jsx: true,\n        },\n        ecmaVersion: 11,\n        sourceType: 'module',\n    },\n    plugins: [\n        'react',\n        'simple-import-sort',\n    ],\n    settings: {\n        react: {\n            version: '16.13.1',\n        },\n    },\n    rules: {\n        indent: ['error', 4],\n        'import/no-cycle': [0],\n        // Unable to resolve path to module 'react'\n        'import/no-unresolved': [0],\n        // Need to add role attribute for accessibility on HTML elements.\n        'jsx-a11y/no-static-element-interactions': [0],\n        'jsx-a11y/click-events-have-key-events': [0],\n        'jsx-a11y/mouse-events-have-key-events': [0],\n        'jsx-a11y/anchor-is-valid': [0],\n        'jsx-a11y/no-noninteractive-tabindex': [0],\n        'no-param-reassign': [0],\n        'no-underscore-dangle': [0, 'allowAfterThis'],\n        'no-unused-vars': ['error', { args: 'none', varsIgnorePattern: '^_' }],\n        'react/jsx-indent': ['error', 4],\n        'react/jsx-indent-props': ['error', 4],\n        'react/destructuring-assignment': [0],\n        'react/jsx-filename-extension': [0],\n        'react/jsx-props-no-spreading': [0],\n        'react/no-unused-class-component-methods': [0],\n        // Otherwise, every non-required propType would need defaultValue.\n        'react/require-default-props': [0],\n        'no-restricted-exports': [0],\n        'simple-import-sort/imports': 'error',\n    },\n};\n"
  },
  {
    "path": "config/example.glados.json",
    "content": "{\n    \"database\": {\n        \"dialect\": \"sqlite\",\n        \"storage\": \"data/test.sqlite\",\n        \"logging\": false\n    },\n    \"backup\": {\n        \"location\": \"data\",\n        \"save_interval_ms\": null\n    },\n    \"server\": {\n        \"host\": \"localhost\",\n        \"port\": 8080\n    }\n}\n"
  },
  {
    "path": "config/jest.config.js",
    "content": "const path = require('path');\n\nmodule.exports = {\n    rootDir: '..',\n    roots: ['src'],\n    testRegex: 'test.js',\n    transform: {\n        '\\\\.js$': ['babel-jest', { configFile: path.join(__dirname, 'babel.config.js') }],\n    },\n};\n"
  },
  {
    "path": "config/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst nodeExternals = require('webpack-node-externals');\n\nconst path = require('path');\n\nfunction fromProjectRoot(relativePath) {\n    return path.resolve(__dirname, '..', relativePath);\n}\n\nfunction getJSModuleRule() {\n    return {\n        test: /\\.(js|ts)$/,\n        use: [\n            {\n                loader: 'babel-loader',\n                options: {\n                    configFile: path.join(__dirname, 'babel.config.js'),\n                },\n            },\n        ],\n        exclude: /node_modules/,\n    };\n}\n\nfunction getStats() {\n    return {\n        assets: true, // Show generated bundles.\n        builtAt: true, // The one signal I actually want.\n        children: false,\n        entrypoints: false,\n        hash: false,\n        modules: false, // Show all the modules that are part of this package.\n        timings: false,\n        version: false,\n    };\n}\n\nfunction getClientSideBundle(entryPoint, outputFileName) {\n    return {\n        mode: 'development',\n        entry: fromProjectRoot(entryPoint),\n        output: {\n            path: fromProjectRoot('dist'),\n            filename: outputFileName,\n        },\n        devServer: {\n            hot: true,\n        },\n        resolve: {\n            extensions: ['.js', '.css'],\n            fallback: {\n                assert: require.resolve('assert'),\n                util: require.resolve('util'),\n            },\n        },\n        module: {\n            rules: [\n                getJSModuleRule(),\n                {\n                    test: /\\.css$/,\n                    use: [\n                        MiniCssExtractPlugin.loader,\n                        // The css-loader interprets @import and url()\n                        // like import/require() and will resolve them.\n                        'css-loader',\n                    ],\n                },\n            ],\n        },\n        plugins: [\n            new webpack.ProvidePlugin({\n                process: 'process/browser',\n            }),\n            new MiniCssExtractPlugin({\n                filename: 'index.css',\n            }),\n            new HtmlWebpackPlugin({\n                template: fromProjectRoot('src/client/index.html'),\n                favicon: 'src/client/index.ico',\n            }),\n        ],\n        stats: getStats(),\n    };\n}\n\nfunction getServerSideBundle(entryPoint, outputFileName) {\n    return {\n        mode: 'development',\n        entry: fromProjectRoot(entryPoint),\n        output: {\n            path: fromProjectRoot('dist'),\n            filename: outputFileName,\n        },\n        devServer: {\n            hot: true,\n        },\n        resolve: {\n            extensions: ['.js'],\n        },\n        module: {\n            rules: [\n                getJSModuleRule(),\n            ],\n        },\n        // https://medium.com/tomincode/hiding-critical-dependency-warnings-from-webpack-c76ccdb1f6c1\n        plugins: [\n            new webpack.ContextReplacementPlugin(\n                /src\\/server/,\n                (data) => {\n                    // The following error is expected in actions.js to support Jest.\n                    //     Critical dependency: require function is used in a way in\n                    //     which dependencies cannot be statically extracted.\n                    data.dependencies.forEach((dependency) => {\n                        delete dependency.critical;\n                    });\n                    return data;\n                },\n            ),\n        ],\n        stats: getStats(),\n        // https://www.npmjs.com/package/webpack-node-externals\n        target: 'node',\n        externals: [nodeExternals()],\n    };\n}\n\nmodule.exports = [\n    getClientSideBundle('src/client/index.js', 'client.js'),\n    getServerSideBundle('src/server/index.js', 'server.js'),\n    getServerSideBundle('src/demo/index.js', 'demo.js'),\n];\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"glados\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Generic Life Activity Data Organization System\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"webpack --config ./config/webpack.config.js\",\n    \"demo\": \"node ./dist/demo.js\",\n    \"server\": \"node ./dist/server.js\",\n    \"server-watch\": \"nodemon --watch ./dist/server.js --exec node ./dist/server.js\",\n    \"database-reset\": \"yarn run server -a database-reset\",\n    \"backup-save\": \"yarn run server -a backup-save\",\n    \"backup-load\": \"yarn run server -a backup-load\",\n    \"lint\": \"eslint -c ./config/eslint.config.js --ext .js,.jsx --fix src\",\n    \"test\": \"jest --config ./config/jest.config.js --no-watchman\",\n    \"todo\": \"grep -nir '// TODO' src\",\n    \"kill\": \"ps -A | grep \\\"bin/node ./dist\\\" | grep -v grep | awk '{ print \\\"kill -9\\\", $1 }' | zsh\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/kaustubh-karkare/glados.git\"\n  },\n  \"keywords\": [],\n  \"author\": \"Kaustubh Karkare\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/kaustubh-karkare/glados/issues\"\n  },\n  \"homepage\": \"https://github.com/kaustubh-karkare/glados#readme\",\n  \"dependencies\": {\n    \"array-move\": \"^2.2.2\",\n    \"assert\": \"^2.0.0\",\n    \"bootstrap\": \"^4.5.0\",\n    \"classnames\": \"^2.2.6\",\n    \"date-fns\": \"^2.15.0\",\n    \"date-fns-timezone\": \"^0.1.4\",\n    \"deep-equal\": \"^2.0.3\",\n    \"deepcopy\": \"^2.1.0\",\n    \"draft-js\": \"^0.11.5\",\n    \"draft-js-markdown-shortcuts-plugin\": \"^0.6.1\",\n    \"draft-js-mention-plugin\": \"^3.1.5\",\n    \"draft-js-plugins-editor\": \"^3.0.0\",\n    \"express\": \"^5.0.0\",\n    \"markdown-draft-js\": \"^2.2.1\",\n    \"process\": \"^0.11.10\",\n    \"prop-types\": \"^15.7.2\",\n    \"query-string\": \"^6.13.1\",\n    \"react\": \"^16.13.1\",\n    \"react-bootstrap\": \"^1.0.1\",\n    \"react-bootstrap-typeahead\": \"^5.0.0-rc.1\",\n    \"react-date-range\": \"^1.0.3\",\n    \"react-datepicker\": \"^3.0.0\",\n    \"react-dom\": \"^16.13.1\",\n    \"react-icons\": \"^3.10.0\",\n    \"react-sortable-hoc\": \"^1.11.0\",\n    \"recharts\": \"^2.0.9\",\n    \"selenium-webdriver\": \"^4.20.0\",\n    \"sequelize\": \"^6.35.0\",\n    \"single-instance\": \"^0.0.1\",\n    \"socket.io\": \"^4.7.0\",\n    \"socket.io-client\": \"^4.7.0\",\n    \"sqlite3\": \"^5.1.7\",\n    \"timezone-support\": \"^2.0.2\",\n    \"toposort\": \"^2.0.2\",\n    \"util\": \"^0.12.5\",\n    \"yargs\": \"^15.4.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.9.6\",\n    \"@babel/node\": \"^7.8.7\",\n    \"@babel/plugin-proposal-class-properties\": \"^7.10.1\",\n    \"@babel/plugin-transform-runtime\": \"^7.9.6\",\n    \"@babel/preset-env\": \"^7.9.6\",\n    \"@babel/preset-react\": \"^7.9.4\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n    \"@typescript-eslint/parser\": \"^5.62.0\",\n    \"babel-loader\": \"^8.1.0\",\n    \"css-loader\": \"^6.8.0\",\n    \"eslint\": \"^8.56.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-plugin-import\": \"^2.29.0\",\n    \"eslint-plugin-jsx-a11y\": \"^6.8.0\",\n    \"eslint-plugin-react\": \"^7.33.0\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-simple-import-sort\": \"^10.0.0\",\n    \"html-webpack-plugin\": \"^5.5.0\",\n    \"husky\": \"^9.1.0\",\n    \"jest\": \"^29.7.0\",\n    \"mini-css-extract-plugin\": \"^2.7.0\",\n    \"nodemon\": \"^3.1.0\",\n    \"style-loader\": \"^3.3.0\",\n    \"tmp\": \"^0.2.3\",\n    \"ts-loader\": \"^9.4.0\",\n    \"typescript\": \"^5.3.0\",\n    \"walk-sync\": \"^2.2.0\",\n    \"webpack\": \"^5.88.0\",\n    \"webpack-cli\": \"^5.1.0\",\n    \"webpack-node-externals\": \"^3.0.0\"\n  }\n}\n"
  },
  {
    "path": "src/README.md",
    "content": "### Code Organization\n\n* 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).\n* 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.\n* 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.\n* 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.\n* 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.\n\n### Backup File Size Estimation\n\n* (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.\n* 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.\n"
  },
  {
    "path": "src/client/Application/Application.js",
    "content": "import React from 'react';\nimport Col from 'react-bootstrap/Col';\nimport Container from 'react-bootstrap/Container';\nimport Row from 'react-bootstrap/Row';\n\nimport { Enum } from '../../common/data_types';\nimport DateUtils from '../../common/DateUtils';\nimport {\n    Coordinator, DataLoader, DateContext, EnumSelectorSection, ModalStack,\n    PluginDisplayComponent, PluginDisplayLocation, ScrollableSection, SettingsContext,\n} from '../Common';\nimport { LogEventList } from '../LogEvent';\nimport { LogStructureList } from '../LogStructure';\nimport { LogTopicList } from '../LogTopic';\nimport PropTypes from '../prop-types';\nimport { ReminderSidebar } from '../Reminders';\nimport { SettingsSection } from '../Settings';\nimport BackupSection from './BackupSection';\nimport CreditsSection from './CreditsSection';\nimport DetailsSection from './DetailsSection';\nimport FavoritesSection from './FavoritesSection';\nimport IndexSection from './IndexSection';\nimport TabSection from './TabSection';\nimport URLState from './URLState';\n\nconst Layout = Enum([\n    {\n        label: 'Split',\n        value: 'split',\n    },\n    {\n        label: 'Left',\n        value: 'left',\n    },\n    {\n        label: 'Right',\n        value: 'right',\n    },\n]);\n\nconst Widgets = Enum([\n    {\n        label: 'Show',\n        value: 'show',\n    },\n    {\n        label: 'Hide',\n        value: 'hide',\n    },\n]);\n\nclass Applicaton extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { urlParams: null, settings: null, disabled: false };\n        this.tabRef = React.createRef();\n    }\n\n    componentDidMount() {\n        this.deregisterCallbacks = [\n            URLState.init(),\n            Coordinator.subscribe('url-change', (urlParams) => this.setState({ urlParams })),\n        ];\n        const urlParams = Coordinator.invoke('url-params');\n        urlParams.tab = urlParams.tab || TabSection.Enum.LOG_EVENT;\n        urlParams.layout = urlParams.layout || Layout.SPLIT;\n        urlParams.widgets = urlParams.widgets || Widgets.SHOW;\n        this.setState({ urlParams });\n\n        this.dataLoader = new DataLoader({\n            getInput: () => ({ name: 'settings-get' }),\n            onData: (settings) => this.setState({ settings }),\n        });\n    }\n\n    componentDidUpdate() {\n        this.dataLoader.reload();\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());\n    }\n\n    renderLeftSidebar() {\n        return (\n            <Col md={2} className=\"my-3\">\n                <ScrollableSection>\n                    <TabSection\n                        plugins={this.props.plugins}\n                        value={this.state.urlParams.tab}\n                        onChange={(tab) => Coordinator.invoke('url-update', { tab })}\n                        ref={this.tabRef}\n                    />\n                    {this.state.urlParams.widgets === Widgets.SHOW\n                        ? (\n                            <ReminderSidebar disabled={this.state.disabled} />\n                        )\n                        : null}\n                </ScrollableSection>\n            </Col>\n        );\n    }\n\n    renderCenterSection() {\n        const { settings } = this.state;\n        const { layout } = this.state.urlParams;\n        let indexSection = null;\n        if (this.tabRef.current) {\n            const Component = this.tabRef.current.getComponent(this.state.urlParams.tab);\n            indexSection = (\n                <IndexSection\n                    Component={Component}\n                    dateRange={this.state.urlParams.dateRange}\n                    search={this.state.urlParams.search}\n                    disabled={this.state.disabled}\n                    onChange={(params) => Coordinator.invoke('url-update', params)}\n                />\n            );\n        } else {\n            setTimeout(() => this.forceUpdate(), 0);\n        }\n        let detailsSection = (\n            <DetailsSection\n                item={this.state.urlParams.details}\n                disabled={this.state.disabled}\n                onChange={(details) => Coordinator.invoke('url-update', { details })}\n            />\n        );\n        if (settings.display_two_details_sections) {\n            detailsSection = (\n                <ScrollableSection>\n                    <DetailsSection\n                        item={this.state.urlParams.details}\n                        disabled={this.state.disabled}\n                        onChange={(details) => Coordinator.invoke('url-update', { details })}\n                    />\n                    <div className=\"py-4\" />\n                    <DetailsSection\n                        item={this.state.urlParams.details2}\n                        disabled={this.state.disabled}\n                        onChange={(details2) => Coordinator.invoke('url-update', { details2 })}\n                    />\n                </ScrollableSection>\n            );\n        }\n        if (layout === Layout.SPLIT) {\n            return (\n                <>\n                    <Col md={4} className=\"my-3\">{indexSection}</Col>\n                    <Col md={4} className=\"my-3\">{detailsSection}</Col>\n                </>\n            );\n        } if (layout === Layout.LEFT) {\n            return (\n                <Col md={8} className=\"my-3\">{indexSection}</Col>\n            );\n        } if (layout === Layout.RIGHT) {\n            return (\n                <Col md={8} className=\"my-3\">{detailsSection}</Col>\n            );\n        }\n        return null;\n    }\n\n    renderRightSidebar() {\n        const { settings } = this.state;\n        const results = [];\n        results.push(\n            <PluginDisplayComponent\n                key={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_TOP}\n                plugins={this.props.plugins}\n                location={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_TOP}\n            />,\n        );\n        results.push(\n            <EnumSelectorSection\n                key=\"layout\"\n                label=\"Layout: \"\n                options={Layout.Options}\n                value={this.state.urlParams.layout}\n                onChange={(layout) => Coordinator.invoke('url-update', { layout })}\n            />,\n            <EnumSelectorSection\n                key=\"widgets\"\n                label=\"Widgets: \"\n                options={Widgets.Options}\n                value={this.state.urlParams.widgets}\n                onChange={(widgets) => Coordinator.invoke('url-update', { widgets })}\n            />,\n            <BackupSection key=\"backup\" />,\n        );\n        if (settings) {\n            results.push(\n                <SettingsSection\n                    key=\"settings\"\n                    settings={settings}\n                    plugins={this.props.plugins}\n                />,\n            );\n        }\n        results.push(\n            <PluginDisplayComponent\n                key={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_BOTTOM}\n                plugins={this.props.plugins}\n                location={PluginDisplayLocation.RIGHT_SIDEBAR_MAIN_BOTTOM}\n            />,\n        );\n        if (this.state.urlParams.widgets === Widgets.SHOW) {\n            results.push(...this.renderRightSidebarWidgets());\n        }\n        results.push(<CreditsSection key=\"credit\" />);\n        return (\n            <Col md={2} className=\"my-3\">\n                <ScrollableSection>\n                    {results}\n                </ScrollableSection>\n            </Col>\n        );\n    }\n\n    renderRightSidebarWidgets() {\n        const nameSortComparator = (left, right) => left.name.localeCompare(right.name);\n        const results = [];\n        results.push(\n            <PluginDisplayComponent\n                key={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_TOP}\n                plugins={this.props.plugins}\n                location={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_TOP}\n            />,\n        );\n        results.push(\n            <div key=\"favorites\">\n                <FavoritesSection\n                    title=\"Favorite Events\"\n                    dataType=\"log-event\"\n                    ViewerComponent={LogEventList.Single}\n                    viewerComponentProps={{ viewerComponentProps: { displayDate: true } }}\n                    valueKey=\"logEvent\"\n                />\n                <FavoritesSection\n                    title=\"Favorite Topics\"\n                    dataType=\"log-topic\"\n                    sortComparator={nameSortComparator}\n                    ViewerComponent={LogTopicList.Single}\n                    valueKey=\"logTopic\"\n                />\n                <FavoritesSection\n                    title=\"Favorite Structures\"\n                    dataType=\"log-structure\"\n                    sortComparator={nameSortComparator}\n                    ViewerComponent={LogStructureList.Single}\n                    valueKey=\"logStructure\"\n                />\n            </div>,\n        );\n        results.push(\n            <PluginDisplayComponent\n                key={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_BOTTOM}\n                plugins={this.props.plugins}\n                location={PluginDisplayLocation.RIGHT_SIDEBAR_WIDGETS_BOTTOM}\n            />,\n        );\n        return results;\n    }\n\n    render() {\n        if (!this.state.urlParams) {\n            return null;\n        } if (!this.state.settings) {\n            return null;\n        }\n        const container = (\n            <Container fluid>\n                <Row>\n                    {this.renderLeftSidebar()}\n                    {this.renderCenterSection(this.state.urlParams.layout)}\n                    {this.renderRightSidebar()}\n                </Row>\n                <ModalStack />\n            </Container>\n        );\n        return (\n            <SettingsContext.Provider value={this.state.settings}>\n                <DateContext.Provider value={DateUtils.getContext(this.state.settings)}>\n                    {container}\n                </DateContext.Provider>\n            </SettingsContext.Provider>\n        );\n    }\n}\n\nApplicaton.propTypes = {\n    plugins: PropTypes.Custom.Plugins.isRequired,\n};\n\nexport default Applicaton;\n"
  },
  {
    "path": "src/client/Application/BackupSection.js",
    "content": "import React from 'react';\n\nimport {\n    Coordinator, DataLoader, LeftRight, SidebarSection,\n} from '../Common';\n\nclass BackupSection extends React.Component {\n    static onClick() {\n        window.api.send('backup-save')\n            .then(({ isUnchanged }) => Coordinator.invoke('modal-info', {\n                title: 'Backup',\n                message: isUnchanged ? 'Backup unchanged!' : 'Backup complete!',\n            }));\n    }\n\n    constructor(props) {\n        super(props);\n        this.state = { latestBackup: null };\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => ({\n                name: 'backup-latest',\n            }),\n            onData: (latestBackup) => this.setState({ latestBackup }),\n        });\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    render() {\n        const { latestBackup } = this.state;\n        return (\n            <SidebarSection>\n                <LeftRight>\n                    <a\n                        className=\"mr-2\"\n                        href=\"#\"\n                        onClick={() => BackupSection.onClick()}\n                        title=\"Save New Backup\"\n                    >\n                        Backup:\n                    </a>\n                    {latestBackup ? `${latestBackup.timetamp}` : 'No backup found!' }\n                </LeftRight>\n            </SidebarSection>\n        );\n    }\n}\n\nexport default BackupSection;\n"
  },
  {
    "path": "src/client/Application/CreditsSection.js",
    "content": "import React from 'react';\n\nimport { SidebarSection } from '../Common';\n\nfunction CreditsSection(props) {\n    return (\n        <SidebarSection>\n            {'Built by: '}\n            <a href=\"http://kaustubh.io\">\n                Kaustubh Karkare\n            </a>\n            {' | '}\n            <a href=\"https://github.com/kaustubh-karkare/glados\">\n                GitHub\n            </a>\n        </SidebarSection>\n    );\n}\n\nexport default CreditsSection;\n"
  },
  {
    "path": "src/client/Application/DetailsSection.css",
    "content": ".details-section .scrollable-section .text-editor {\n    background-color: var(--component-color);\n    padding: 4px;\n}\n\n.details-section .scrollable-section .public-DraftEditor-content {\n    min-height: 200px;\n}\n"
  },
  {
    "path": "src/client/Application/DetailsSection.js",
    "content": "import './DetailsSection.css';\n\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport {\n    MdCheckCircle, MdClose, MdEdit, MdFavorite, MdFavoriteBorder, MdSearch,\n} from 'react-icons/md';\nimport { RiLoaderLine } from 'react-icons/ri';\n\nimport RichTextUtils from '../../common/RichTextUtils';\nimport {\n    Coordinator, DataLoader, debounce,\n    ScrollableSection, SettingsContext, TextEditor, TypeaheadOptions, TypeaheadSelector,\n} from '../Common';\nimport { LogEventDetailsHeader, LogEventEditor } from '../LogEvent';\nimport { LogValueListEditor } from '../LogKey';\nimport { LogStructureDetailsHeader, LogStructureEditor } from '../LogStructure';\nimport { LogTopicDetailsHeader, LogTopicEditor, LogTopicOptions } from '../LogTopic';\nimport PropTypes from '../prop-types';\n\nconst HEADER_MAPPING = {\n    'log-event': {\n        HeaderComponent: LogEventDetailsHeader,\n        EditorComponent: LogEventEditor,\n        valueKey: 'logEvent',\n    },\n    'log-structure': {\n        HeaderComponent: LogStructureDetailsHeader,\n        EditorComponent: LogStructureEditor,\n        valueKey: 'logStructure',\n    },\n    'log-topic': {\n        HeaderComponent: LogTopicDetailsHeader,\n        EditorComponent: LogTopicEditor,\n        valueKey: 'logTopic',\n    },\n};\n\nclass DetailsSection extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            item: null,\n            isDirty: false,\n            isSaveDisabled: false,\n        };\n        this.saveDebounced = debounce(this.saveNotDebounced, 500);\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => {\n                const { item } = this.props;\n                if (!item) {\n                    return null;\n                } if (item.__type__ in HEADER_MAPPING) {\n                    return {\n                        name: `${item.__type__}-load`,\n                        args: { __id__: item.__id__ },\n                    };\n                }\n                return null;\n            },\n            onData: (newItem) => {\n                const oldItem = this.state.item;\n                if (\n                    oldItem\n                    && newItem\n                    && oldItem.__type__ === newItem.__type__\n                    && oldItem.__id__ === newItem.__id__\n                ) {\n                    this.setState((state) => {\n                        const { details } = state.item; // copy local details\n                        state.item = { ...newItem, details };\n                        return state;\n                    });\n                } else {\n                    this.setState({ item: newItem });\n                }\n            },\n            onError: () => {\n                const { item } = this.props;\n                if (item) {\n                    Coordinator.invoke(\n                        'modal-error',\n                        `${JSON.stringify(item, null, 4)}\\n\\nThis item does support details!`,\n                    );\n                }\n                this.props.onChange(null);\n            },\n        });\n    }\n\n    componentDidUpdate() {\n        this.dataLoader.reload();\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    onChange(item) {\n        this.setState((state) => {\n            state.item = item;\n            state.isDirty = true;\n            return state;\n        }, this.saveDebounced);\n    }\n\n    onEditButtonClick() {\n        const { item } = this.state;\n        const { EditorComponent, valueKey } = HEADER_MAPPING[item.__type__];\n        Coordinator.invoke('modal-editor', {\n            dataType: item.__type__,\n            EditorComponent,\n            valueKey,\n            value: item,\n        });\n    }\n\n    saveNotDebounced() {\n        if (this.state.isSaveDisabled) {\n            return;\n        }\n        const { item } = this.state;\n        if (item) {\n            window.api.send(`${item.__type__}-upsert`, item)\n                .then((newItem) => this.setState({\n                    isDirty: !RichTextUtils.equals(item.details, newItem.details),\n                }));\n        }\n    }\n\n    renderPrefixButtons(item) {\n        const buttons = [];\n        const { HeaderComponent } = HEADER_MAPPING[item.__type__];\n        if (HeaderComponent.onSearchButtonClick) {\n            buttons.push(\n                <Button\n                    key=\"search\"\n                    onClick={() => HeaderComponent.onSearchButtonClick(item)}\n                    title=\"Search\"\n                >\n                    <MdSearch />\n                </Button>,\n            );\n        }\n        if (typeof item.isFavorite === 'boolean') {\n            buttons.push(\n                <Button\n                    key=\"favorite\"\n                    onClick={() => this.onChange({ ...item, isFavorite: !item.isFavorite })}\n                    title=\"Favorite\"\n                >\n                    {item.isFavorite ? <MdFavorite /> : <MdFavoriteBorder />}\n                </Button>,\n            );\n        }\n        return buttons;\n    }\n\n    renderSuffixButtons(item) {\n        return [\n            <Button key=\"edit\" title=\"Edit\" onClick={() => this.onEditButtonClick()}>\n                <MdEdit />\n            </Button>,\n            <Button key=\"status\" title=\"Status\">\n                {this.state.isDirty ? <RiLoaderLine /> : <MdCheckCircle />}\n            </Button>,\n            <Button\n                key=\"close\"\n                title=\"Close\"\n                onClick={() => this.props.onChange(null)}\n            >\n                <MdClose />\n            </Button>,\n        ];\n    }\n\n    renderHeader() {\n        const { item } = this.state;\n        if (item && item.__type__ in HEADER_MAPPING) {\n            const { HeaderComponent, valueKey } = HEADER_MAPPING[item.__type__];\n            const headerComponentProps = { [valueKey]: item };\n            return (\n                <InputGroup>\n                    {this.renderPrefixButtons(item)}\n                    <HeaderComponent {...headerComponentProps} />\n                    {this.renderSuffixButtons(item)}\n                </InputGroup>\n            );\n        }\n\n        const options = new TypeaheadOptions({\n            serverSideOptions: [\n                { name: 'log-topic' },\n                { name: 'log-structure' },\n            ],\n        });\n        return (\n            <InputGroup>\n                <TypeaheadSelector\n                    id=\"details-section-topic-or-structure\"\n                    options={options}\n                    value={null}\n                    disabled={this.props.disabled}\n                    onChange={(newItem) => this.props.onChange(newItem)}\n                    placeholder=\"Details ...\"\n                />\n            </InputGroup>\n        );\n    }\n\n    renderKeys() {\n        const { item } = this.state;\n        let logKeys = null;\n        if (!item) {\n            // nothing\n        } else if (item.__type__ === 'log-event') {\n            logKeys = item.logStructure && item.logStructure.logKeys;\n        } else if (item.__type__ === 'log-topic') {\n            logKeys = item.parentLogTopic && item.parentLogTopic.childKeys;\n        }\n        if (!logKeys) {\n            return null;\n        }\n        return (\n            <LogValueListEditor\n                source={item}\n                logKeys={logKeys}\n                disabled\n                onChange={() => null}\n            />\n        );\n    }\n\n    renderDetails() {\n        const { item } = this.state;\n        if (!item) {\n            return null;\n        }\n        if (\n            item.__type__ === 'log-event'\n            && item.logStructure\n            && !item.logStructure.eventAllowDetails\n        ) {\n            return <div>(disabled by structure)</div>;\n        }\n        const parentLogTopic = item && item.__type__ === 'log-topic' ? item : undefined;\n        return (\n            <div>\n                <TextEditor\n                    unstyled\n                    value={item.details}\n                    onChange={(details) => this.onChange({ ...item, details })}\n                    options={LogTopicOptions.get({\n                        allowCreation: true,\n                        parentLogTopic,\n                        beforeSelect: () => this.setState({ isSaveDisabled: true }),\n                        afterSelect: () => this.setState({ isSaveDisabled: false }),\n                    })}\n                />\n            </div>\n        );\n    }\n\n    render() {\n        const settings = this.context;\n        if (settings.display_two_details_sections) {\n            return (\n                <div className=\"details-section\">\n                    <div className=\"mb-1\">\n                        {this.renderHeader()}\n                    </div>\n                    {this.renderKeys()}\n                    {this.renderDetails()}\n                </div>\n            );\n        }\n        return (\n            <div className=\"details-section\">\n                <div className=\"mb-1\">\n                    {this.renderHeader()}\n                </div>\n                <ScrollableSection padding={20 + 4}>\n                    {this.renderDetails()}\n                </ScrollableSection>\n            </div>\n        );\n    }\n}\n\nDetailsSection.propTypes = {\n    item: PropTypes.Custom.Item,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nDetailsSection.contextType = SettingsContext;\n\nexport default DetailsSection;\n"
  },
  {
    "path": "src/client/Application/FavoritesSection.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { DataLoader, SidebarSection } from '../Common';\n\nclass FavoritesSection extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { items: null };\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => ({\n                name: `${this.props.dataType}-list`,\n                args: {\n                    where: { isFavorite: true },\n                },\n            }),\n            onData: (items) => {\n                if (this.props.sortComparator) {\n                    items = items.sort(this.props.sortComparator);\n                }\n                this.setState({ items });\n            },\n        });\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    renderContent() {\n        if (this.state.items === null) {\n            return 'Loading ...';\n        }\n        const { ViewerComponent, viewerComponentProps, valueKey } = this.props;\n        return this.state.items.map((item) => (\n            <ViewerComponent\n                key={item.__id__}\n                {...viewerComponentProps}\n                {...{ [valueKey]: item }}\n            />\n        ));\n    }\n\n    render() {\n        return (\n            <SidebarSection title={this.props.title}>\n                {this.renderContent()}\n            </SidebarSection>\n        );\n    }\n}\n\nFavoritesSection.propTypes = {\n    title: PropTypes.string.isRequired,\n    dataType: PropTypes.string.isRequired,\n    sortComparator: PropTypes.func,\n    ViewerComponent: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    viewerComponentProps: PropTypes.object,\n    valueKey: PropTypes.string.isRequired,\n};\n\nFavoritesSection.defaultProps = {\n    viewerComponentProps: {},\n};\n\nexport default FavoritesSection;\n"
  },
  {
    "path": "src/client/Application/IndexSection.css",
    "content": ".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",
    "content": "import React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { DateRangePicker, ScrollableSection, TypeaheadSelector } from '../Common';\nimport PropTypes from '../prop-types';\n\nclass IndexSection extends React.Component {\n    renderWithTypeahead() {\n        const { Component, dateRange, onChange } = this.props;\n        const typeaheadOptions = Component.getTypeaheadOptions();\n        const filteredSearch = typeaheadOptions.filterToKnownTypes(this.props.search);\n        if (filteredSearch.length !== this.props.search.length) {\n            window.setTimeout(onChange.bind(filteredSearch), 0);\n        }\n        return (\n            <div className=\"index-section\">\n                <div className=\"mb-1\">\n                    <InputGroup>\n                        <DateRangePicker\n                            dateRange={dateRange}\n                            onChange={(newDateRange) => onChange({ dateRange: newDateRange })}\n                        />\n                        <TypeaheadSelector\n                            id=\"search\"\n                            options={typeaheadOptions}\n                            value={filteredSearch}\n                            disabled={this.props.disabled}\n                            onChange={(search) => onChange({ search })}\n                            placeholder=\"Search ...\"\n                            multiple\n                        />\n                    </InputGroup>\n                </div>\n                <ScrollableSection padding={20 + 4}>\n                    <Component\n                        dateRange={dateRange}\n                        search={filteredSearch}\n                    />\n                </ScrollableSection>\n            </div>\n        );\n    }\n\n    renderSimple() {\n        const { Component } = this.props;\n        return (\n            <div className=\"index-section\">\n                <ScrollableSection>\n                    <Component />\n                </ScrollableSection>\n            </div>\n        );\n    }\n\n    render() {\n        const { Component } = this.props;\n        if (Component.getTypeaheadOptions) {\n            return this.renderWithTypeahead();\n        }\n        return this.renderSimple();\n    }\n}\n\nIndexSection.propTypes = {\n    Component: PropTypes.func.isRequired,\n    dateRange: PropTypes.Custom.DateRange,\n    search: PropTypes.arrayOf(PropTypes.Custom.Item.isRequired).isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default IndexSection;\n"
  },
  {
    "path": "src/client/Application/TabSection.js",
    "content": "import React from 'react';\n\nimport { Enum } from '../../common/data_types';\nimport { PluginDisplayLocation, SettingsContext, SidebarSection } from '../Common';\nimport { GraphSection } from '../Graphs';\nimport { LogEventSearch } from '../LogEvent';\nimport { LogStructureSearch } from '../LogStructure';\nimport { LogTopicSearch } from '../LogTopic';\nimport PropTypes from '../prop-types';\n\nconst Tab = Enum([\n    {\n        label: 'Manage Events',\n        value: 'log-event',\n        Component: LogEventSearch,\n    },\n    {\n        label: 'Manage Topics',\n        value: 'log-topic',\n        Component: LogTopicSearch,\n    },\n    {\n        label: 'Manage Structures',\n        value: 'log-structure',\n        Component: LogStructureSearch,\n    },\n    {\n        label: 'Explore Graphs',\n        value: 'graph',\n        Component: GraphSection,\n    },\n]);\n\nclass TabSection extends React.Component {\n    constructor(props) {\n        super(props);\n        const PluginOptions = [];\n        const TabComponents = {};\n        Tab.Options.forEach((option) => {\n            TabComponents[option.value] = option.Component;\n        });\n        Object.entries(this.props.plugins).forEach(([_name, api]) => {\n            if (api.getDisplayLocation() === PluginDisplayLocation.TAB_SECTION) {\n                const tabData = api.getTabData();\n                PluginOptions.push(tabData);\n                TabComponents[tabData.value] = () => (\n                    <SettingsContext.Consumer>\n                        {(settings) => {\n                            const key = api.getSettingsKey();\n                            return api.getDisplayComponent({\n                                settings: key ? settings[key] : null,\n                            });\n                        }}\n                    </SettingsContext.Consumer>\n                );\n            }\n        });\n        this.state = {\n            options: Tab.Options.concat(PluginOptions),\n            components: TabComponents,\n        };\n    }\n\n    getComponent(value) {\n        return this.state.components[value];\n    }\n\n    render() {\n        return this.state.options.map((option) => (\n            <SidebarSection\n                key={option.value}\n                onClick={() => this.props.onChange(option.value)}\n                selected={this.props.value === option.value}\n            >\n                {option.label}\n            </SidebarSection>\n        ));\n    }\n}\n\nTabSection.Enum = Tab;\n\nTabSection.propTypes = {\n    plugins: PropTypes.Custom.Plugins.isRequired,\n    value: PropTypes.string.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default TabSection;\n"
  },
  {
    "path": "src/client/Application/URLState.js",
    "content": "import { Coordinator, DateRangePicker, URLManager } from '../Common';\n\n/**\n * [...Array(128).keys()]\n *     .map(code => String.fromCharCode(code))\n *     .filter(char => !char.match(/\\w/) && char === encodeURIComponent(char))\n * [\"!\", \"'\", \"(\", \")\", \"*\", \"-\", \".\", \"~\"]\n * Picked the one most easily readable in the URL.\n */\nconst SEPARATOR = '~';\n\nfunction serializeItem(item) {\n    return `${item.__type__}${SEPARATOR}${item.__id__}${SEPARATOR}${item.name}`;\n}\n\nfunction deserializeItem(token) {\n    const [__type__, __id__, name] = token.split(SEPARATOR);\n    return { __type__, __id__: parseInt(__id__, 10), name };\n}\n\nclass URLState {\n    static getStateFromURL() {\n        const params = URLManager.get();\n        return {\n            tab: params.tab,\n            layout: params.layout,\n            widgets: params.widgets,\n            dateRange: DateRangePicker.deserialize(params.date_range),\n            search: params.search ? params.search.map(deserializeItem) : [],\n            details: params.details ? deserializeItem(params.details) : null,\n            // settings.display_two_details_sections\n            details2: params.details2 ? deserializeItem(params.details2) : null,\n        };\n    }\n\n    static getURLFromState(state) {\n        const params = {\n            tab: state.tab,\n            layout: state.layout,\n            widgets: state.widgets,\n            date_range: DateRangePicker.serialize(state.dateRange),\n            search: state.search ? state.search.map(serializeItem) : undefined,\n            details: state.details ? serializeItem(state.details) : undefined,\n            // settings.display_two_details_sections\n            details2: state.details2 ? serializeItem(state.details2) : undefined,\n        };\n        return URLManager.getLink(params);\n    }\n\n    static init() {\n        const instance = new URLState();\n        return () => instance.cleanup();\n    }\n\n    constructor() {\n        this.deregisterCallbacks = [\n            URLManager.init(() => this.onChange()),\n            Coordinator.register('url-params', () => this.state),\n            Coordinator.register('url-link', (data) => this.getLink(data)),\n            Coordinator.register('url-update', (data) => this.onUpdate(data)),\n        ];\n        this.onChange(); // set this.state\n    }\n\n    cleanup() {\n        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());\n    }\n\n    onChange() {\n        this.state = URLState.getStateFromURL();\n        Coordinator.broadcast('url-change', this.state);\n    }\n\n    getLink(methodOrData) {\n        let newState;\n        if (typeof methodOrData === 'function') {\n            newState = methodOrData(this.state) || this.state;\n        } else {\n            newState = { ...this.state, ...methodOrData };\n        }\n        return URLState.getURLFromState(newState);\n    }\n\n    onUpdate(methodOrData) {\n        URLManager.update(this.getLink(methodOrData));\n    }\n}\n\nexport default URLState;\n"
  },
  {
    "path": "src/client/Application/index.js",
    "content": "// eslint-disable-next-line import/prefer-default-export\nexport { default as Application } from './Application';\n"
  },
  {
    "path": "src/client/Bootstrap/InputGroup.css",
    "content": ".input-group:focus {\n    outline: none;\n}\n\n.input-group > * {\n    border-style: solid;\n    border-color: transparent;\n    border-radius: 0;\n    border-width: 0px 0px;\n    font-size: var(--font-size);\n    height: 20px;\n}\n\n.input-group *:focus {\n    outline: none;\n}\n\n.input-group > :first-child {\n    border-top-left-radius: 2px;\n    border-bottom-left-radius: 2px;\n    border-left-width: 0;\n}\n\n.input-group > :last-child {\n    border-top-right-radius: 2px;\n    border-bottom-right-radius: 2px;\n    border-right-width: 0;\n}\n\n.input-group > .input-group-text {\n    background: var(--component-color);\n    display: block;\n    padding: 1px;\n    text-align: center;\n    width: 128px;\n}\n\n.input-group > .btn {\n    background: var(--input-background-color);\n    width: 20px;\n    padding: 0px;\n}\n\n.input-group > .btn > svg {\n    position: relative;\n    top: -1px;\n}\n\n.input-group > input.form-control {\n    background-color: var(--input-background-color);\n    color: var(--input-text-color);\n    height: 20px;\n    padding: 0 4px;\n}\n\n.input-group > select.form-control {\n    background-color: var(--input-background-color);\n    color: var(--input-text-color);\n    padding: 0;\n}\n\n.input-group > .form-check-inline {\n    margin-right: 0;\n    width: 16px;\n}\n\n.input-group > .rbt input:first-child {\n    border: none;\n    border-radius: 0;\n    border-width: 0 1px;\n    padding: 0 4px;\n    height: 20px;\n}\n\n.input-group > .rbt input:first-child[disabled] {\n    background-color: var(--input-disabled-background-color);;\n}\n\n.input-group > .text-editor {\n    flex-grow: 1;\n    width: 1px;\n    height: auto;\n}\n"
  },
  {
    "path": "src/client/Bootstrap/Modal.css",
    "content": ".modal-dialog {\n    max-width: 800px;\n}\n\n.modal-content {\n    background-color: var(--background-color);\n    border-color: var(--component-highlight-color);\n    color: var(--text-color);\n}\n"
  },
  {
    "path": "src/client/Bootstrap/Popover.css",
    "content": ".popover {\n    max-width: none;\n}\n\n.popover-header,\n.popover-body {\n    background-color: var(--input-background-color);\n}\n"
  },
  {
    "path": "src/client/Bootstrap/index.js",
    "content": "import 'bootstrap/dist/css/bootstrap.min.css';\nimport './InputGroup.css';\nimport './Modal.css';\nimport './Popover.css';\n"
  },
  {
    "path": "src/client/Common/AddLinkPlugin.js",
    "content": "// https://bitwiser.in/2017/05/11/creating-rte-part-3-entities-and-decorators.html\n\n/* eslint-disable */\n\nimport React from 'react';\nimport {\n    RichUtils,\n    KeyBindingUtil,\n    EditorState,\n} from 'draft-js';\n\n\nexport const linkStrategy = (contentBlock, callback, contentState) => {\n    contentBlock.findEntityRanges(\n        (character) => {\n            const entityKey = character.getEntity();\n            return (\n                entityKey !== null\n        && contentState.getEntity(entityKey).getType() === 'LINK'\n            );\n        },\n        callback,\n    );\n};\n\n\nexport const Link = (props) => {\n    const { contentState, entityKey } = props;\n    const { url } = contentState.getEntity(entityKey).getData();\n    return (\n        <a\n            className=\"link\"\n            href={url}\n            rel=\"noopener noreferrer\"\n            target=\"_blank\"\n            aria-label={url}\n        >\n            {props.children}\n        </a>\n    );\n};\n\nconst AddLinkPlugin = {\n    keyBindingFn(event, { getEditorState }) {\n        const editorState = getEditorState();\n        const selection = editorState.getSelection();\n        // Don't do anything if no text is selected.\n        if (selection.isCollapsed()) {\n            return;\n        }\n        if (KeyBindingUtil.hasCommandModifier(event) && event.which === 75) {\n            return 'add-link';\n        }\n    },\n\n    handleKeyCommand(command, editorState, eventTimeStamp, { getEditorState, setEditorState }) {\n        if (command !== 'add-link') {\n            return 'not-handled';\n        }\n        const link = window.prompt('Paste the link:');\n        const selection = editorState.getSelection();\n        if (!link) {\n            setEditorState(RichUtils.toggleLink(editorState, selection, null));\n            return 'handled';\n        }\n        const content = editorState.getCurrentContent();\n        const contentWithEntity = content.createEntity('LINK', 'MUTABLE', { url: link });\n        const newEditorState = EditorState.push(editorState, contentWithEntity, 'create-entity');\n        const entityKey = contentWithEntity.getLastCreatedEntityKey();\n        setEditorState(RichUtils.toggleLink(newEditorState, selection, entityKey));\n        return 'handled';\n    },\n\n    decorators: [{\n        strategy: linkStrategy,\n        component: Link,\n    }],\n};\n\nexport default AddLinkPlugin;\n"
  },
  {
    "path": "src/client/Common/AsyncSelector.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\n\nimport DataLoader from './DataLoader';\n\nclass AsyncSelector extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { options: null };\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => this.props.options,\n            onData: (options) => this.setState({\n                options: [...this.props.prefixOptions, ...options, ...this.props.suffixOptions],\n            }),\n        });\n    }\n\n    componentDidUpdate(prevProps) {\n        this.dataLoader.reload();\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    onChange(id) {\n        if (this.state.options) {\n            const selectedOption = this.state.options.find(\n                (option) => option.id.toString() === id,\n            );\n            if (selectedOption) {\n                this.props.onChange(selectedOption);\n            }\n        }\n    }\n\n    render() {\n        const options = this.state.options || [this.props.value];\n        return (\n            <Form.Control\n                as=\"select\"\n                value={this.props.value.__id__}\n                disabled={this.props.disabled}\n                onChange={(event) => this.onChange(event.target.value)}\n            >\n                {options.map((item) => {\n                    const optionProps = { key: item.__id__, value: item.__id__ };\n                    return <option {...optionProps}>{item[this.props.labelKey]}</option>;\n                })}\n            </Form.Control>\n        );\n    }\n}\n\nAsyncSelector.propTypes = {\n    labelKey: PropTypes.string,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.object,\n    // eslint-disable-next-line react/forbid-prop-types\n    prefixOptions: PropTypes.array,\n    // eslint-disable-next-line react/forbid-prop-types\n    options: PropTypes.object,\n    // eslint-disable-next-line react/forbid-prop-types\n    suffixOptions: PropTypes.array,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nAsyncSelector.defaultProps = {\n    labelKey: 'name',\n    prefixOptions: [],\n    suffixOptions: [],\n};\n\nexport default AsyncSelector;\n"
  },
  {
    "path": "src/client/Common/BulletList/BulletList.css",
    "content": ".bullet-list .pager {\n    color: var(--text-disabled-color);\n}\n\n.bullet-list .pager > span {\n    cursor: pointer;\n}\n\n.bullet-list .pager > span:hover {\n    color: var(--text-color);\n}\n"
  },
  {
    "path": "src/client/Common/BulletList/BulletList.js",
    "content": "import './BulletList.css';\n\nimport arrayMove from 'array-move';\nimport classNames from 'classnames';\nimport deepEqual from 'deep-equal';\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport { SortableContainer, SortableElement } from 'react-sortable-hoc';\n\nimport { getDataTypeMapping } from '../../../common/data_types';\nimport DataLoader from '../DataLoader';\nimport SettingsContext from '../SettingsContext';\nimport BulletListItem from './BulletListItem';\nimport BulletListLine from './BulletListLine';\nimport BulletListPager from './BulletListPager';\nimport BulletListTitle from './BulletListTitle';\n\nconst WrappedContainer = SortableContainer(({ children }) => <div>{children}</div>);\nconst SortableBulletListItem = SortableElement(BulletListItem);\n\nclass BulletList extends React.Component {\n    static getDerivedStateFromProps(props, state) {\n        if (state.items) {\n            state.areAllExpanded = state.items\n                .every((item) => state.isExpanded[item.__id__]);\n        }\n        return state;\n    }\n\n    constructor(props) {\n        super(props);\n        const pageSize = parseInt(props.settings.bullet_list_page_size, 10) || 25;\n        this.state = {\n            items: null,\n            isExpanded: {},\n            areAllExpanded: true,\n            pageSize,\n            limit: pageSize,\n        };\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => ({\n                name: `${this.props.dataType}-list`,\n                args: {\n                    where: this.props.where,\n                    limit: this.state.limit !== null ? this.state.limit + 1 : undefined,\n                },\n            }),\n            onData: (items) => {\n                if (this.state.limit && items.length > this.state.limit) {\n                    this.setState({ items: items.slice(1), hasMoreItems: true });\n                } else {\n                    this.setState({ items, hasMoreItems: false, limit: null });\n                }\n            },\n        });\n    }\n\n    componentDidUpdate(prevProps) {\n        if (\n            prevProps.dataType !== this.props.dataType\n            || !deepEqual(prevProps.where, this.props.where)\n        ) {\n            this.updateLimit(this.state.pageSize);\n        }\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    onAddButtonClick(event) {\n        const DataType = getDataTypeMapping()[this.props.dataType];\n        const value = DataType.createVirtual(this.props.where);\n        const context = { ...this };\n        context.props = { ...context.props, value };\n        BulletListItem.prototype.onEdit.call(context, event);\n    }\n\n    onSortButtonClick(event) {\n        const input = {\n            dataType: this.props.dataType,\n            where: this.props.where,\n        };\n        window.api.send(`${this.props.dataType}-sort`, input);\n    }\n\n    onMove(index, delta, event) {\n        if (!event.shiftKey) return;\n        const otherIndex = index + delta;\n        const totalLength = this.state.items.length;\n        if (otherIndex < 0 || otherIndex === totalLength) return;\n        this.onReorder({ oldIndex: index, newIndex: otherIndex });\n    }\n\n    onReorder({ oldIndex, newIndex }) {\n        if (!this.props.allowReordering) return;\n        const orderedItems = arrayMove(this.state.items, oldIndex, newIndex);\n        const input = {\n            dataType: this.props.dataType,\n            where: this.props.where,\n            ordering: orderedItems.map((item) => item.__id__),\n        };\n        window.api.send(`${this.props.dataType}-reorder`, input)\n            .then(() => this.setState({ items: orderedItems }));\n    }\n\n    updateLimit(limit) {\n        this.setState({ limit, items: null }, () => this.dataLoader.reload());\n    }\n\n    renderItems() {\n        if (!this.state.items) {\n            return (\n                <BulletListLine>\n                    <span>Loading ...</span>\n                </BulletListLine>\n            );\n        }\n        return this.state.items.map((item, index) => (\n            <SortableBulletListItem\n                index={index}\n                key={item.__id__}\n                dataType={this.props.dataType}\n                valueKey={this.props.valueKey}\n                ViewerComponent={this.props.ViewerComponent}\n                viewerComponentProps={this.props.viewerComponentProps}\n                EditorComponent={this.props.EditorComponent}\n                allowReordering={this.props.allowReordering}\n                prefixActions={this.props.prefixActions\n                    .map((action) => ({ ...action, perform: action.perform.bind(null, item) }))}\n                onMoveUp={(event) => this.onMove(index, -1, event)}\n                onMoveDown={(event) => this.onMove(index, 1, event)}\n                isExpanded={this.state.isExpanded[item.__id__] || false}\n                setIsExpanded={(isExpanded) => this.setState((state) => {\n                    state.isExpanded[item.__id__] = isExpanded;\n                    return state;\n                })}\n                value={item}\n                dragHandleSpace\n            />\n        ));\n    }\n\n    renderAdder() {\n        const { AdderComponent } = this.props;\n        if (!AdderComponent) {\n            return null;\n        }\n        return (\n            <BulletListLine>\n                <AdderComponent where={this.props.where} />\n            </BulletListLine>\n        );\n    }\n\n    render() {\n        return (\n            <div className={classNames('bullet-list', this.props.className)}>\n                <BulletListTitle\n                    name={this.props.name}\n                    areAllExpanded={this.state.areAllExpanded}\n                    onToggleButtonClick={() => this.setState((state) => {\n                        if (state.areAllExpanded) {\n                            return { isExpanded: {} };\n                        }\n                        return {\n                            isExpanded: Object.fromEntries(\n                                state.items.map((item) => [item.__id__, true]),\n                            ),\n                        };\n                    })}\n                    onAddButtonClick={this.props.allowCreation\n                        ? (event) => this.onAddButtonClick(event)\n                        : null}\n                    onSortButtonClick={this.props.allowSorting\n                        ? (event) => this.onSortButtonClick(event)\n                        : null}\n                />\n                <BulletListPager\n                    batchSize={this.state.pageSize}\n                    limit={this.state.limit}\n                    updateLimit={(limit) => this.updateLimit(limit)}\n                    itemsLength={this.state.items ? this.state.items.length : null}\n                    hasMoreItems={this.state.hasMoreItems}\n                />\n                <WrappedContainer\n                    helperClass=\"sortableDraggedItem\"\n                    useDragHandle\n                    onSortEnd={(data) => this.onReorder(data)}\n                >\n                    {this.renderItems()}\n                </WrappedContainer>\n                {this.renderAdder()}\n            </div>\n        );\n    }\n}\n\nBulletList.propTypes = {\n    name: PropTypes.string.isRequired,\n    dataType: PropTypes.string.isRequired,\n    valueKey: PropTypes.string.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    where: PropTypes.object,\n    allowCreation: PropTypes.bool,\n    allowSorting: PropTypes.bool,\n    allowReordering: PropTypes.bool,\n    ViewerComponent: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    viewerComponentProps: PropTypes.object,\n    EditorComponent: PropTypes.func.isRequired,\n    AdderComponent: PropTypes.func,\n    // eslint-disable-next-line react/forbid-prop-types\n    prefixActions: PropTypes.array,\n    className: PropTypes.string,\n    // eslint-disable-next-line react/forbid-prop-types\n    settings: PropTypes.object.isRequired,\n};\n\nBulletList.defaultProps = {\n    allowCreation: true,\n    prefixActions: [],\n};\n\nconst WrappedBulletList = SettingsContext.Wrapper(BulletList);\nWrappedBulletList.Item = BulletListItem;\n\nexport default WrappedBulletList;\n"
  },
  {
    "path": "src/client/Common/BulletList/BulletListIcon.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { KeyCodes } from '../Utils';\n\nfunction BulletListIcon(props) {\n    return (\n        <div\n            className=\"icon ml-1\"\n            title={props.title}\n            onClick={props.onClick}\n            onKeyDown={(event) => {\n                if (event.keyCode === KeyCodes.ENTER) {\n                    props.onClick(event);\n                }\n            }}\n        >\n            {props.children}\n        </div>\n    );\n}\n\nBulletListIcon.propTypes = {\n    onClick: PropTypes.func.isRequired,\n    title: PropTypes.string.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any.isRequired,\n};\n\nexport default BulletListIcon;\n"
  },
  {
    "path": "src/client/Common/BulletList/BulletListItem.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { BsList } from 'react-icons/bs';\nimport { GoPrimitiveDot } from 'react-icons/go';\nimport { MdEdit, MdFormatLineSpacing } from 'react-icons/md';\nimport { TiMinus, TiPlus } from 'react-icons/ti';\nimport { SortableHandle } from 'react-sortable-hoc';\n\nimport Coordinator from '../Coordinator';\nimport Dropdown from '../Dropdown';\nimport Highlightable from '../Highlightable';\nimport Icon from '../Icon';\nimport InputLine from '../InputLine';\nimport { KeyCodes } from '../Utils';\n\nconst SortableDragHandle = SortableHandle(() => (\n    <Icon className=\"sortableDragHandle\" title=\"Reorder\">\n        <MdFormatLineSpacing />\n    </Icon>\n));\n\nclass BulletListItem extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { isHighlighted: false, isExpanded: false };\n        this.dropdownRef = React.createRef();\n    }\n\n    onEdit(event) {\n        if (event) {\n            // Don't let enter propagate to EditorModal.\n            event.preventDefault();\n            event.stopPropagation();\n        }\n        if (event && event.shiftKey) {\n            Coordinator.invoke('url-update', { details: this.props.value });\n            return;\n        }\n        Coordinator.invoke('modal-editor', {\n            dataType: this.props.dataType,\n            EditorComponent: this.props.EditorComponent,\n            valueKey: this.props.valueKey,\n            value: this.props.value,\n        });\n    }\n\n    onDelete(event) {\n        if (event) {\n            // Don't let enter propagate to ConfirmationModal.\n            event.preventDefault();\n            event.stopPropagation();\n        }\n        if (event && !event.shiftKey) {\n            Coordinator.invoke('modal-confirm', {\n                title: 'Confirm deletion?',\n                body: this.renderViewer(),\n                onClose: (result) => {\n                    if (result) this.onDelete();\n                },\n            });\n            return;\n        }\n        window.api.send(`${this.props.dataType}-delete`, this.props.value.__id__);\n    }\n\n    onKeyDown(event) {\n        if (event.keyCode === KeyCodes.SPACE) {\n            this.setIsExpanded(!this.getIsExpanded());\n        } else if (event.keyCode === KeyCodes.ENTER) {\n            this.onEdit(event);\n        } else if (event.keyCode === KeyCodes.DELETE) {\n            this.onDelete(event);\n        } else if (event.keyCode === KeyCodes.UP_ARROW) {\n            if (this.props.allowReordering) this.props.onMoveUp(event);\n        } else if (event.keyCode === KeyCodes.DOWN_ARROW) {\n            if (this.props.allowReordering) this.props.onMoveDown(event);\n        }\n    }\n\n    getIsExpanded() {\n        if (typeof this.props.isExpanded !== 'undefined') {\n            return this.props.isExpanded;\n        }\n        return this.state.isExpanded;\n    }\n\n    setIsExpanded(isExpanded) {\n        if (typeof this.props.isExpanded !== 'undefined') {\n            this.props.setIsExpanded(isExpanded);\n        } else {\n            this.setState({ isExpanded });\n        }\n    }\n\n    setIsHighlighted(isHighlighted) {\n        if (!isHighlighted && this.dropdownRef.current) {\n            this.dropdownRef.current.hide();\n        }\n        this.setState({ isHighlighted });\n    }\n\n    getViewerProps() {\n        return { [this.props.valueKey]: this.props.value, ...this.props.viewerComponentProps };\n    }\n\n    renderDragHandle() {\n        if (!this.props.dragHandleSpace) return null;\n        if (this.state.isHighlighted && this.props.allowReordering) return <SortableDragHandle />;\n        return <Icon />;\n    }\n\n    renderBullet() {\n        const isExpanded = this.getIsExpanded();\n        const iconProps = {\n            alwaysHighlighted: true,\n            className: 'mr-1',\n            title: isExpanded ? 'Collapse' : 'Expand',\n        };\n        if (this.state.isHighlighted) {\n            return (\n                <Icon {...iconProps} onClick={() => this.setIsExpanded(!isExpanded)}>\n                    {isExpanded ? <TiMinus /> : <TiPlus />}\n                </Icon>\n            );\n        }\n        return (\n            <Icon {...iconProps}>\n                {isExpanded ? <TiMinus /> : <GoPrimitiveDot />}\n            </Icon>\n        );\n    }\n\n    renderEditButton() {\n        if (!this.state.isHighlighted) {\n            return null;\n        }\n        return <MdEdit onClick={(event) => this.onEdit(event)} />;\n    }\n\n    renderActionsDropdown() {\n        if (!this.state.isHighlighted) {\n            return null;\n        }\n        const actions = [...this.props.prefixActions];\n        actions.push({\n            __id__: 'delete',\n            name: 'Delete',\n            perform: (event) => this.onDelete(event),\n        });\n        actions.push({\n            __id__: 'info',\n            name: 'Debug Info',\n            perform: (_event) => Coordinator.invoke(\n                'modal-info',\n                {\n                    title: 'Debug Info',\n                    message: <pre>{JSON.stringify(this.props.value, null, 4)}</pre>,\n                },\n            ),\n        });\n        return (\n            <Dropdown\n                disabled={false}\n                options={actions}\n                onChange={(action, event) => action.perform(event)}\n                ref={this.dropdownRef}\n            >\n                <BsList\n                    onMouseOver={() => {\n                        if (this.dropdownRef.current) {\n                            this.dropdownRef.current.show();\n                        }\n                    }}\n                />\n            </Dropdown>\n        );\n    }\n\n    renderExpanded() {\n        if (!this.getIsExpanded()) {\n            return null;\n        }\n        // 13 = width of 1 icon. 4 = margin right of bullet icon\n        const marginLeft = 13 * (this.props.dragHandleSpace ? 2 : 1) + 4;\n        return (\n            <div style={{ marginLeft }}>\n                {this.renderExpandedViewer()}\n            </div>\n        );\n    }\n\n    renderViewer() {\n        const { ViewerComponent } = this.props;\n        return (\n            <ViewerComponent\n                {...this.getViewerProps()}\n                toggleExpansion={() => this.setIsExpanded(!this.getIsExpanded())}\n            />\n        );\n    }\n\n    renderExpandedViewer() {\n        const { ViewerComponent } = this.props;\n        if (ViewerComponent.Expanded) {\n            return <ViewerComponent.Expanded {...this.getViewerProps()} />;\n        }\n        return null;\n    }\n\n    render() {\n        return (\n            <>\n                <Highlightable\n                    isHighlighted={this.state.isHighlighted}\n                    onChange={(isHighlighted) => this.setIsHighlighted(isHighlighted)}\n                    onKeyDown={(event) => this.onKeyDown(event)}\n                >\n                    <InputGroup>\n                        {this.renderDragHandle()}\n                        {this.renderBullet()}\n                        <InputLine>{this.renderViewer()}</InputLine>\n                        <Icon className=\"ml-1\" title=\"Edit\">\n                            {this.renderEditButton()}\n                        </Icon>\n                        <Icon className=\"ml-1\" title=\"Actions\">\n                            {this.renderActionsDropdown()}\n                        </Icon>\n                    </InputGroup>\n                </Highlightable>\n                {this.renderExpanded()}\n            </>\n        );\n    }\n}\n\nBulletListItem.propTypes = {\n    dataType: PropTypes.string.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.object.isRequired,\n    valueKey: PropTypes.string.isRequired,\n    ViewerComponent: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    viewerComponentProps: PropTypes.object,\n    EditorComponent: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    prefixActions: PropTypes.array,\n\n    // The following props are only used by BulletList.\n    dragHandleSpace: PropTypes.bool,\n    allowReordering: PropTypes.bool,\n    onMoveUp: PropTypes.func,\n    onMoveDown: PropTypes.func,\n    isExpanded: PropTypes.bool,\n    setIsExpanded: PropTypes.func,\n};\n\nBulletListItem.defaultProps = {\n    prefixActions: [],\n};\n\nexport default BulletListItem;\n"
  },
  {
    "path": "src/client/Common/BulletList/BulletListLine.js",
    "content": "import React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { GoPrimitiveDot } from 'react-icons/go';\n\nfunction BulletListLine(props) {\n    // eslint-disable-next-line react/prop-types\n    const { children, ...moreProps } = props;\n    return (\n        <InputGroup {...moreProps}>\n            <div className=\"icon\" />\n            <div className=\"icon mr-1\">\n                <GoPrimitiveDot />\n            </div>\n            {children}\n        </InputGroup>\n    );\n}\n\nexport default BulletListLine;\n"
  },
  {
    "path": "src/client/Common/BulletList/BulletListPager.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport Highlightable from '../Highlightable';\nimport { KeyCodes } from '../Utils';\nimport BulletListLine from './BulletListLine';\n\nclass BulletListPager extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            isHighlighted: false,\n        };\n    }\n\n    onKeyDown(event) {\n        if (event.keyCode === KeyCodes.SPACE) {\n            this.props.updateLimit(this.props.limit + this.props.batchSize);\n        } else if (event.keyCode === KeyCodes.ENTER) {\n            this.props.updateLimit(null);\n        }\n    }\n\n    renderButtons() {\n        return (\n            <>\n                {' |'}\n                <span\n                    className=\"mx-1\"\n                    onClick={() => this.props.updateLimit(this.props.limit + this.props.batchSize)}\n                >\n                    Load More\n                </span>\n                |\n                <span\n                    className=\"mx-1\"\n                    onClick={() => this.props.updateLimit(null)}\n                >\n                    Load All\n                </span>\n            </>\n        );\n    }\n\n    render() {\n        let message;\n        if (this.props.itemsLength === null) {\n            if (this.props.limit === this.props.batchSize) {\n                // We don't know whether we need pagination yet.\n                return null;\n            } if (this.props.limit) {\n                message = `Fetching last ${this.props.limit} items ...`;\n            } else {\n                message = 'Fetching all items ...';\n            }\n            return (\n                <BulletListLine className=\"pager\">\n                    {message}\n                </BulletListLine>\n            );\n        }\n        if (this.props.itemsLength <= this.props.batchSize && !this.props.hasMoreItems) {\n            // No need for pagination.\n            return null;\n        }\n        let buttons;\n        if (this.props.hasMoreItems) {\n            message = `Showing last ${this.props.itemsLength} items`;\n            buttons = this.renderButtons();\n        } else {\n            message = `Showing all ${this.props.itemsLength} items`;\n        }\n        return (\n            <Highlightable\n                isHighlighted={this.state.isHighlighted}\n                onChange={(isHighlighted) => this.setState({ isHighlighted })}\n            >\n                <BulletListLine className=\"pager\">\n                    {message}\n                    {buttons}\n                </BulletListLine>\n            </Highlightable>\n        );\n    }\n}\n\nBulletListPager.propTypes = {\n    batchSize: PropTypes.number.isRequired,\n    limit: PropTypes.number,\n    updateLimit: PropTypes.func.isRequired,\n    itemsLength: PropTypes.number,\n    hasMoreItems: PropTypes.bool,\n};\n\nexport default BulletListPager;\n"
  },
  {
    "path": "src/client/Common/BulletList/BulletListTitle.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { MdAddCircleOutline } from 'react-icons/md';\nimport { TiMinus, TiPlus } from 'react-icons/ti';\n\nimport Highlightable from '../Highlightable';\nimport { KeyCodes } from '../Utils';\nimport BulletListIcon from './BulletListIcon';\n\nclass BulletListTitle extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { isHighlighted: false };\n    }\n\n    onKeyDown(event) {\n        if (event.keyCode === KeyCodes.ENTER) {\n            this.props.onAddButtonClick(event);\n        }\n    }\n\n    renderListToggleButton() {\n        if (this.props.areAllExpanded) {\n            return (\n                <BulletListIcon\n                    title=\"Collapse All\"\n                    onClick={this.props.onToggleButtonClick}\n                >\n                    <TiMinus />\n                </BulletListIcon>\n            );\n        }\n        return (\n            <BulletListIcon\n                title=\"Expand All\"\n                onClick={this.props.onToggleButtonClick}\n            >\n                <TiPlus />\n            </BulletListIcon>\n        );\n    }\n\n    renderAddButton() {\n        if (!this.props.onAddButtonClick) {\n            return null;\n        }\n        return (\n            <BulletListIcon\n                title=\"Create New\"\n                onClick={this.props.onAddButtonClick}\n            >\n                <MdAddCircleOutline />\n            </BulletListIcon>\n        );\n    }\n\n    renderSortButton() {\n        if (!this.props.onSortButtonClick) {\n            return null;\n        }\n        // TODO: Use a proper icon to indicate sorting.\n        // Was on a flight (no internet access) when I added this feature.\n        return (\n            <BulletListIcon\n                title=\"Sort\"\n                onClick={this.props.onSortButtonClick}\n            >\n                <MdAddCircleOutline />\n            </BulletListIcon>\n        );\n    }\n\n    render() {\n        return (\n            <Highlightable\n                isHighlighted={this.state.isHighlighted}\n                onChange={(isHighlighted) => this.setState({ isHighlighted })}\n                onKeyDown={(event) => this.onKeyDown(event)}\n            >\n                <InputGroup>\n                    <div>{this.props.name}</div>\n                    {this.state.isHighlighted ? this.renderListToggleButton() : null}\n                    {this.state.isHighlighted ? this.renderAddButton() : null}\n                    {this.state.isHighlighted ? this.renderSortButton() : null}\n                </InputGroup>\n            </Highlightable>\n        );\n    }\n}\n\nBulletListTitle.propTypes = {\n    name: PropTypes.string.isRequired,\n    areAllExpanded: PropTypes.bool.isRequired,\n    onToggleButtonClick: PropTypes.func.isRequired,\n    onAddButtonClick: PropTypes.func,\n    onSortButtonClick: PropTypes.func,\n};\n\nexport default BulletListTitle;\n"
  },
  {
    "path": "src/client/Common/BulletList/index.js",
    "content": "export { default } from './BulletList';\n"
  },
  {
    "path": "src/client/Common/ConfirmModal.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport Modal from 'react-bootstrap/Modal';\n\nimport { suppressUnlessShiftKey } from './Utils';\n\nfunction ConfirmModal(props) {\n    return (\n        <Modal\n            show\n            onHide={() => props.onClose()}\n            onEscapeKeyDown={suppressUnlessShiftKey}\n        >\n            <Modal.Header closeButton>\n                <Modal.Title>{props.title}</Modal.Title>\n            </Modal.Header>\n            <Modal.Body>\n                {props.body}\n            </Modal.Body>\n            <Modal.Footer>\n                <Button onClick={() => props.onClose(false)}>\n                    {props.noLabel}\n                </Button>\n                {' '}\n                <Button onClick={() => props.onClose(true)}>\n                    {props.yesLabel}\n                </Button>\n            </Modal.Footer>\n        </Modal>\n    );\n}\n\nConfirmModal.propTypes = {\n    title: PropTypes.string.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    body: PropTypes.any.isRequired,\n    yesLabel: PropTypes.string,\n    noLabel: PropTypes.string,\n    onClose: PropTypes.func.isRequired,\n};\n\nConfirmModal.defaultProps = {\n    yesLabel: 'Yes',\n    noLabel: 'No',\n};\n\nexport default ConfirmModal;\n"
  },
  {
    "path": "src/client/Common/Coordinator.js",
    "content": "const callbacks = {};\n\nclass Coordinator {\n    static register(name, callback) {\n        callbacks[name] = callback;\n        return () => delete callbacks[name];\n    }\n\n    static invoke(name, ...args) {\n        return callbacks[name].call(this, ...args);\n    }\n\n    static subscribe(name, callback) {\n        if (!(name in callbacks)) {\n            callbacks[name] = [];\n        }\n        callbacks[name].push(callback);\n        return () => {\n            const index = callbacks[name].indexOf(callback);\n            callbacks[name].splice(index, 1);\n        };\n    }\n\n    static broadcast(name, ...args) {\n        if (!(name in callbacks)) {\n            callbacks[name] = [];\n        }\n        callbacks[name].forEach((callback) => callback.call(this, ...args));\n    }\n}\n\nexport default Coordinator;\n"
  },
  {
    "path": "src/client/Common/DataLoader.js",
    "content": "import deepEqual from 'deep-equal';\nimport deepcopy from 'deepcopy';\n\nimport { getPartialItem, isItem } from '../../common/data_types';\n\nfunction IGNORE() {\n    return null;\n}\n\nclass DataLoader {\n    constructor({ getInput, onData, onError }) {\n        this.getInput = getInput;\n        this.input = null;\n        this.cancelSubscription = null;\n        this.onData = onData || IGNORE;\n        this.onError = onError || IGNORE;\n        this.isMounted = true;\n        this.reload();\n    }\n\n    reload({ force } = {}) {\n        const input = deepcopy(this.getInput());\n        if (input && input.args && input.args.where) {\n            // This is an optimization to prevent sending unnecessary data to the server.\n            Object.entries(input.args.where).forEach(([key, value]) => {\n                if (isItem(value)) {\n                    input.args.where[key] = getPartialItem(value);\n                }\n            });\n        }\n        if (!force && deepEqual(input, this.input)) {\n            return;\n        }\n        this.input = input;\n        if (this.input === null) {\n            this.onData(null);\n            return;\n        }\n        window.api.send(this.input.name, this.input.args)\n            .then((data) => {\n                if (this.isMounted) {\n                    this.setupSubscription();\n                    this.onData(data);\n                }\n            })\n            .catch((error) => {\n                if (this.isMounted) {\n                    this.onError(error);\n                }\n            });\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    _compare(name, left, right) {\n        if (name.endsWith('-load')) {\n            return left.__id__ === right.__id__;\n        } if (name.endsWith('-list')) {\n            left = left.where || {};\n            right = right.where || {};\n            return Object.keys(left).every(\n                (key) => typeof right[key] === 'undefined' || left[key] === right[key],\n            );\n        }\n        return true;\n    }\n\n    setupSubscription() {\n        const { promise, cancel } = window.api.subscribe(this.input.name);\n        if (this.cancelSubscription) {\n            this.cancelSubscription();\n        }\n        this.cancelSubscription = cancel;\n        promise.then((data) => {\n            if (!this.isMounted || !this.input) {\n                return;\n            }\n            const queryArgs = this.input.args || {};\n            const broadcastArgs = data || {};\n            if (this._compare(this.input.name, queryArgs, broadcastArgs)) {\n                this.reload({ force: true });\n            } else {\n                this.setupSubscription();\n            }\n        });\n    }\n\n    stop() {\n        this.isMounted = false;\n        if (this.cancelSubscription) {\n            this.cancelSubscription();\n        }\n    }\n}\n\nexport default DataLoader;\n"
  },
  {
    "path": "src/client/Common/DateContext.js",
    "content": "import React from 'react';\n\nconst DateContext = React.createContext(null);\n\nDateContext.Wrapper = (Component) => (moreProps) => (\n    <DateContext.Consumer>\n        {(dateContext) => <Component {...dateContext} {...moreProps} />}\n    </DateContext.Consumer>\n);\n\nexport default DateContext;\n"
  },
  {
    "path": "src/client/Common/DatePicker.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport { Calendar } from 'react-date-range';\n\nimport DateUtils from '../../common/DateUtils';\nimport DateContext from './DateContext';\nimport PopoverElement from './PopoverElement';\n\n// https://github.com/hypeserver/react-date-range\n// Note: The corresponding CSS is included from DateRangePicker.\n\nclass DatePicker extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            lastDate: this.props.date || null,\n        };\n    }\n\n    render() {\n        const { todayLabel } = this.context;\n        const lastDate = this.state.lastDate || todayLabel;\n        return (\n            <PopoverElement onReset={() => this.props.onChange(null)}>\n                {this.props.date || 'Date: Unspecified'}\n                <Calendar\n                    date={DateUtils.getDate(this.props.date || lastDate)}\n                    onChange={(rawDate) => {\n                        const date = DateUtils.getLabel(rawDate);\n                        this.setState({ lastDate: date });\n                        this.props.onChange(date);\n                    }}\n                />\n            </PopoverElement>\n        );\n    }\n}\n\nDatePicker.propTypes = {\n    date: PropTypes.string,\n    onChange: PropTypes.func.isRequired,\n};\n\nDatePicker.contextType = DateContext;\n\nexport default DatePicker;\n"
  },
  {
    "path": "src/client/Common/DateRangePicker.js",
    "content": "// https://adphorus.github.io/react-date-range/\nimport 'react-date-range/dist/styles.css'; // main css file\nimport 'react-date-range/dist/theme/default.css'; // theme css file\n\nimport React from 'react';\nimport { DateRangePicker as DateRangePickerOriginal } from 'react-date-range';\n\nimport DateUtils from '../../common/DateUtils';\nimport PropTypes from '../prop-types';\nimport DateContext from './DateContext';\nimport PopoverElement from './PopoverElement';\n\nconst KEY = 'selection';\n\nfunction DateRangeSelector(props) {\n    const { dateRange } = props;\n    return (\n        <DateRangePickerOriginal\n            direction=\"horizontal\"\n            months={1}\n            moveRangeOnFirstSelection={false}\n            showSelectionPreview\n            ranges={[\n                {\n                    key: KEY,\n                    startDate: DateUtils.getDate(dateRange.startDate),\n                    endDate: DateUtils.getDate(dateRange.endDate),\n                },\n            ]}\n            onChange={(ranges) => props.onChange({\n                startDate: DateUtils.getLabel(ranges[KEY].startDate),\n                endDate: DateUtils.getLabel(ranges[KEY].endDate),\n            })}\n        />\n    );\n}\n\nDateRangeSelector.propTypes = {\n    dateRange: PropTypes.Custom.DateRange.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nclass DateRangePicker extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            lastDateRange: this.props.dateRange || null,\n        };\n    }\n\n    renderSummary() {\n        const { dateRange } = this.props;\n        if (!dateRange) {\n            return 'Date Range: Unspecified';\n        }\n        if (dateRange.startDate === dateRange.endDate) {\n            return dateRange.startDate;\n        }\n        return `${dateRange.startDate} to ${dateRange.endDate}`;\n    }\n\n    render() {\n        const { todayLabel } = this.context;\n        const lastDateRange = this.state.lastDateRange || {\n            startDate: todayLabel,\n            endDate: todayLabel,\n        };\n        return (\n            <PopoverElement onReset={() => this.props.onChange(null)}>\n                {this.renderSummary()}\n                <DateRangeSelector\n                    dateRange={this.props.dateRange || lastDateRange}\n                    onChange={(newDateRange) => {\n                        this.setState({ lastDateRange: newDateRange });\n                        this.props.onChange(newDateRange);\n                    }}\n                />\n            </PopoverElement>\n        );\n    }\n}\n\nDateRangePicker.propTypes = {\n    dateRange: PropTypes.Custom.DateRange,\n    onChange: PropTypes.func.isRequired,\n};\n\nDateRangePicker.Selector = DateRangeSelector;\n\nconst DATE_RANGE_SEPARATOR = ' to ';\n\nDateRangePicker.serialize = (dateRange) => {\n    if (!dateRange) return null;\n    return dateRange.startDate + DATE_RANGE_SEPARATOR + dateRange.endDate;\n};\n\nDateRangePicker.deserialize = (value) => {\n    if (!value) return null;\n    const [startDate, endDate] = value.split(DATE_RANGE_SEPARATOR);\n    return { startDate, endDate };\n};\n\nDateRangePicker.contextType = DateContext;\n\nexport default DateRangePicker;\n"
  },
  {
    "path": "src/client/Common/Dropdown.css",
    "content": ".dropdown-toggle::after {\n    display: none;\n}\n\n/**\n * There are multiple reports of this problem.\n * https://stackoverflow.com/q/42046287/903585\n * https://stackoverflow.com/q/18892351/903585\n * None of those solutions worked, so this is what I came up with.\n */\n.dropdown-menu {\n    inset: 0px 0px auto auto !important;\n}\n"
  },
  {
    "path": "src/client/Common/Dropdown.js",
    "content": "import './Dropdown.css';\n\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport Dropdown from 'react-bootstrap/Dropdown';\n\nimport TypeaheadOptions from './TypeaheadOptions';\n\nclass CustomDropdown extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { isShown: false };\n    }\n\n    componentDidMount() {\n        if (Array.isArray(this.props.options)) {\n            this.setState({ items: this.props.options });\n        }\n    }\n\n    onSelect(item, event) {\n        if (this.props.options instanceof TypeaheadOptions) {\n            this.props.options.select(item)\n                .then((adjustedItem) => {\n                    // undefined = no change\n                    // null = cancel operation\n                    if (adjustedItem !== null) {\n                        this.props.onChange(adjustedItem || item, event);\n                    }\n                });\n        } else {\n            this.props.onChange(item, event);\n        }\n    }\n\n    setIsShown(nextIsShown) {\n        if (nextIsShown) {\n            this.show();\n        } else {\n            this.hide();\n        }\n    }\n\n    hide() {\n        this.setState({ isShown: false });\n    }\n\n    show() {\n        if (this.props.options instanceof TypeaheadOptions) {\n            this.props.options.search('')\n                .then((items) => this.setState({ isShown: true, items }));\n        } else {\n            this.setState({ isShown: true });\n        }\n    }\n\n    renderItems() {\n        if (!this.state.items) return null;\n        if (this.state.items.length === 0) {\n            return (\n                <Dropdown.Item disabled>\n                    No Results\n                </Dropdown.Item>\n            );\n        }\n        return this.state.items.map((item) => (\n            <Dropdown.Item\n                key={item.__id__}\n                onMouseDown={(event) => this.onSelect(item, event)}\n            >\n                {item[this.props.labelKey]}\n            </Dropdown.Item>\n        ));\n    }\n\n    render() {\n        return (\n            <Dropdown\n                as=\"span\"\n                onToggle={(isShown) => this.setIsShown(isShown)}\n                show={this.state.isShown}\n            >\n                <Dropdown.Toggle as=\"span\">\n                    {this.props.children}\n                </Dropdown.Toggle>\n                <Dropdown.Menu>\n                    {this.renderItems()}\n                </Dropdown.Menu>\n            </Dropdown>\n        );\n    }\n}\n\nCustomDropdown.propTypes = {\n    labelKey: PropTypes.string,\n    // eslint-disable-next-line react/no-unused-prop-types\n    disabled: PropTypes.bool.isRequired,\n    options: PropTypes.oneOfType([\n        PropTypes.instanceOf(TypeaheadOptions),\n        PropTypes.array,\n    ]).isRequired,\n    onChange: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nCustomDropdown.defaultProps = {\n    labelKey: 'name',\n};\n\nexport default CustomDropdown;\n"
  },
  {
    "path": "src/client/Common/EditorModal.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport Modal from 'react-bootstrap/Modal';\n\nimport LeftRight from './LeftRight';\nimport { debounce, KeyCodes, suppressUnlessShiftKey } from './Utils';\n\nclass EditorModal extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            value: props.value,\n            status: 'Pending Validation ...',\n            isSaving: false,\n            isValidating: false,\n        };\n        this.validateItemDebounced = debounce(this.validateItemNotDebounced, 500);\n    }\n\n    componentDidMount() {\n        this.validateItemDebounced();\n    }\n\n    onChange(value) {\n        this.setState({ value }, () => this.validateItemDebounced());\n    }\n\n    onSave() {\n        this.saveItemNotDebounced();\n    }\n\n    onClose() {\n        this.props.onClose(this.state.value);\n    }\n\n    validateItemNotDebounced() {\n        this.setState({ isValidating: true, status: 'Validating ...' });\n        window.api.send(`${this.props.dataType}-validate`, this.state.value)\n            .finally(() => this.setState({ isValidating: false }))\n            .then((validationErrors) => this.setState({\n                status: validationErrors.join('\\n') || 'No validation errors!',\n            }))\n            .catch(() => this.setState({ status: 'Error!' }));\n    }\n\n    saveItemNotDebounced() {\n        this.setState({ isSaving: true, status: 'Saving ...' });\n        let promise;\n        if (this.props.onSave) {\n            // A custom onSave method is used for because reminder completion needs to\n            // create the event and update the structure as part of same single transaction.\n            promise = this.props.onSave(this.state.value);\n            if (!(promise instanceof Promise)) {\n                // If the custom onSave method does not return a promise,\n                // it is assumed that the component will be unmounted.\n                return;\n            }\n        } else {\n            promise = window.api.send(`${this.props.dataType}-upsert`, this.state.value);\n        }\n        promise\n            .finally(() => this.setState({ isSaving: false }))\n            .then((value) => {\n                this.setState({ status: 'Saved!', value });\n                this.onClose();\n            })\n            .catch(() => this.setState({ status: 'Error!' }));\n    }\n\n    renderSaveButton() {\n        return (\n            <Button\n                disabled={this.state.isSaving || this.state.isValidating}\n                onClick={() => this.onSave()}\n                style={{ width: '50px' }}\n            >\n                Save\n            </Button>\n        );\n    }\n\n    render() {\n        if (!this.props.value) {\n            return null;\n        }\n        const { EditorComponent, editorProps } = this.props;\n        editorProps[this.props.valueKey] = this.state.value;\n        editorProps.disabled = this.state.isSaving;\n        return (\n            <Modal\n                show\n                onHide={() => this.onClose()}\n                onEscapeKeyDown={suppressUnlessShiftKey}\n            >\n                <Modal.Header closeButton>\n                    <Modal.Title>Editor</Modal.Title>\n                </Modal.Header>\n                <Modal.Body>\n                    <EditorComponent\n                        {...editorProps}\n                        onChange={(newValue) => this.onChange(newValue)}\n                        onSpecialKeys={(event) => {\n                            if (!event.shiftKey) return;\n                            if (event.keyCode === KeyCodes.ENTER) {\n                                this.onSave();\n                            } else if (event.keyCode === KeyCodes.ESCAPE) {\n                                this.onClose();\n                            }\n                        }}\n                    />\n                </Modal.Body>\n                <Modal.Body>\n                    <LeftRight>\n                        <div style={{ whiteSpace: 'pre-wrap' }}>\n                            {this.state.status}\n                        </div>\n                        <InputGroup>\n                            {this.renderSaveButton()}\n                        </InputGroup>\n                    </LeftRight>\n                </Modal.Body>\n            </Modal>\n        );\n    }\n}\n\nEditorModal.propTypes = {\n    dataType: PropTypes.string.isRequired,\n    EditorComponent: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    valueKey: PropTypes.string.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.object.isRequired,\n    onClose: PropTypes.func.isRequired, // provided by ModalStack\n\n    // eslint-disable-next-line react/forbid-prop-types\n    editorProps: PropTypes.object,\n    onSave: PropTypes.func,\n};\n\nEditorModal.defaultProps = {\n    editorProps: {},\n};\n\nexport default EditorModal;\n"
  },
  {
    "path": "src/client/Common/EnumSelectorSection.js",
    "content": "import React from 'react';\n\nimport PropTypes from '../prop-types';\nimport LeftRight from './LeftRight';\nimport SidebarSection from './SidebarSection';\n\nclass EnumSelectorSection extends React.Component {\n    renderOptions() {\n        return this.props.options.map((option, index) => {\n            let { label } = option;\n            if (this.props.value !== option.value) {\n                label = (\n                    <a href=\"#\" onClick={() => this.props.onChange(option.value)}>\n                        {option.label}\n                    </a>\n                );\n            }\n            return (\n                <span key={option.value}>\n                    {index ? ' | ' : ''}\n                    {' '}\n                    {label}\n                </span>\n            );\n        });\n    }\n\n    render() {\n        return (\n            <SidebarSection>\n                <LeftRight>\n                    <div className=\"mr-2\">{this.props.label}</div>\n                    <div>{this.renderOptions()}</div>\n                </LeftRight>\n            </SidebarSection>\n        );\n    }\n}\n\nEnumSelectorSection.propTypes = {\n    label: PropTypes.string.isRequired,\n    options: PropTypes.Custom.EnumOptions.isRequired,\n    value: PropTypes.string.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default EnumSelectorSection;\n"
  },
  {
    "path": "src/client/Common/ErrorModal.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Modal from 'react-bootstrap/Modal';\n\nimport { suppressUnlessShiftKey } from './Utils';\n\nfunction ErrorModal(props) {\n    let { error } = props;\n    if (typeof error !== 'string') {\n        error = JSON.stringify(error);\n    }\n    return (\n        <Modal\n            show\n            onHide={props.onClose}\n            onEscapeKeyDown={suppressUnlessShiftKey}\n        >\n            <Modal.Header closeButton>\n                <Modal.Title>Error</Modal.Title>\n            </Modal.Header>\n            <Modal.Body>\n                <pre>\n                    {error}\n                </pre>\n            </Modal.Body>\n        </Modal>\n    );\n}\n\nErrorModal.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    error: PropTypes.any.isRequired,\n    onClose: PropTypes.func.isRequired,\n};\n\nexport default ErrorModal;\n"
  },
  {
    "path": "src/client/Common/Highlightable.css",
    "content": "\n.highlightable:focus {\n    outline: none;\n}\n\n.highlightable.highlighted {\n    background: var(--component-highlight-color);\n}\n"
  },
  {
    "path": "src/client/Common/Highlightable.js",
    "content": "import './Highlightable.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nimport Coordinator from './Coordinator';\n\nclass Highlightable extends React.Component {\n    constructor(props) {\n        super(props);\n        this.ref = React.createRef();\n    }\n\n    componentDidMount() {\n        this.deregisterCallbacks = [\n            Coordinator.subscribe('unhighlight', () => this.setHighlight(false)),\n        ];\n    }\n\n    componentWillUnmount() {\n        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());\n    }\n\n    setHighlight(isHighlighted) {\n        if (this.props.isHighlighted === isHighlighted) {\n            return;\n        }\n        if (isHighlighted) {\n            Coordinator.broadcast('unhighlight');\n            this.ref.current.focus();\n        }\n        this.props.onChange(isHighlighted);\n    }\n\n    render() {\n        const {\n            isHighlighted, onChange: _, children, ...moreProps\n        } = this.props;\n        return (\n            <div\n                {...moreProps}\n                className={classNames({\n                    highlightable: true,\n                    highlighted: isHighlighted,\n                })}\n                tabIndex={0}\n                onMouseEnter={() => this.setHighlight(true)}\n                onMouseLeave={() => this.setHighlight(false)}\n                onFocus={() => this.setHighlight(true)}\n                onBlur={() => this.setHighlight(false)}\n                ref={this.ref}\n            >\n                {children}\n            </div>\n        );\n    }\n}\n\nHighlightable.propTypes = {\n    isHighlighted: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nexport default Highlightable;\n"
  },
  {
    "path": "src/client/Common/Icon.css",
    "content": ".icon {\n    cursor: pointer;\n    height: 20px;\n    position: relative;\n    top: -1px;\n    width: 13px;\n}\n\n.icon > svg {\n    fill: var(--text-disabled-color);\n}\n\n.icon:not(.icon-never-highlight):hover > svg,\n.icon.icon-highlighted > svg {\n    fill: var(--text-color);\n}\n"
  },
  {
    "path": "src/client/Common/Icon.js",
    "content": "import './Icon.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nfunction Icon(props) {\n    const {\n        alwaysHighlighted, neverHighlighted, className, children, ...moreProps\n    } = props;\n    moreProps.className = classNames({\n        icon: true,\n        'icon-highlighted': alwaysHighlighted,\n        'icon-never-highlight': neverHighlighted,\n    }, className);\n    return (\n        <div {...moreProps}>\n            {children}\n        </div>\n    );\n}\n\nIcon.propTypes = {\n    className: PropTypes.string,\n    alwaysHighlighted: PropTypes.bool,\n    neverHighlighted: PropTypes.bool,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nexport default Icon;\n"
  },
  {
    "path": "src/client/Common/InfoModal.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Modal from 'react-bootstrap/Modal';\n\nimport { suppressUnlessShiftKey } from './Utils';\n\nfunction InfoModal(props) {\n    return (\n        <Modal\n            show\n            onHide={props.onClose}\n            onEscapeKeyDown={suppressUnlessShiftKey}\n        >\n            <Modal.Header closeButton>\n                <Modal.Title>{props.title}</Modal.Title>\n            </Modal.Header>\n            <Modal.Body>\n                {props.message}\n            </Modal.Body>\n        </Modal>\n    );\n}\n\nInfoModal.propTypes = {\n    title: PropTypes.string.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    message: PropTypes.any.isRequired,\n    onClose: PropTypes.func.isRequired,\n};\n\nexport default InfoModal;\n"
  },
  {
    "path": "src/client/Common/InputLine.css",
    "content": ".input-line {\n    flex: 1 1 auto;\n    height: auto;\n    overflow: hidden;\n    width: 0px;\n}\n\n.input-line.overflow {\n    overflow: visible;\n}\n\n.input-line.styled {\n    background-color: var(--input-background-color);\n}\n"
  },
  {
    "path": "src/client/Common/InputLine.js",
    "content": "import './InputLine.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nfunction InputLine(props) {\n    const {\n        className, overflow, styled, children, ...moreProps\n    } = props;\n    moreProps.className = classNames({\n        'input-line': true,\n        overflow,\n        styled,\n    }, className);\n    return (\n        <div {...moreProps}>\n            {children}\n        </div>\n    );\n}\n\nInputLine.propTypes = {\n    className: PropTypes.string,\n    overflow: PropTypes.bool,\n    styled: PropTypes.bool,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nexport default InputLine;\n"
  },
  {
    "path": "src/client/Common/LeftRight.js",
    "content": "/* eslint-disable react/prop-types */\n\nimport React from 'react';\n\nfunction LeftRight(props) {\n    return (\n        <div {...props}>\n            <div className=\"d-flex\">\n                <div className=\"mr-auto\">{props.children[0]}</div>\n                <div>{props.children[1]}</div>\n            </div>\n        </div>\n    );\n}\n\nexport default LeftRight;\n"
  },
  {
    "path": "src/client/Common/Link.js",
    "content": "import assert from 'assert';\nimport React from 'react';\n\nimport PropTypes from '../prop-types';\nimport Coordinator from './Coordinator';\n\nfunction Link(props) {\n    const { logStructure, logTopic } = props;\n    assert(!(logStructure && logTopic));\n    const item = logStructure || logTopic;\n    assert(item);\n\n    let link;\n    try {\n        link = Coordinator.invoke('url-link', { details: item });\n    } catch (error) {\n        link = '#';\n    }\n    return (\n        <a\n            className=\"topic\"\n            title={item.name}\n            href={link}\n            tabIndex={-1}\n            onClick={(event) => {\n                event.preventDefault();\n                event.stopPropagation();\n                Coordinator.invoke('url-update', { details: item });\n            }}\n        >\n            {props.children}\n        </a>\n    );\n}\n\nLink.propTypes = {\n    logStructure: PropTypes.Custom.LogStructure,\n    logTopic: PropTypes.Custom.LogTopic,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nexport default Link;\n"
  },
  {
    "path": "src/client/Common/ModalStack.js",
    "content": "import assert from 'assert';\nimport React from 'react';\n\nimport ConfirmModal from './ConfirmModal';\nimport Coordinator from './Coordinator';\nimport EditorModal from './EditorModal';\nimport ErrorModal from './ErrorModal';\nimport InfoModal from './InfoModal';\n\nclass ModalStack extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            components: [],\n            sourceElement: null,\n        };\n    }\n\n    componentDidMount() {\n        this.deregisterCallbacks = [\n            Coordinator.register(\n                'modal-editor',\n                (componentProps) => this.push(EditorModal, componentProps),\n            ),\n            Coordinator.register('modal-confirm', this.push.bind(this, ConfirmModal)),\n            Coordinator.register('modal-error', (error) => this.push(ErrorModal, { error })),\n            Coordinator.register('modal-info', ({ title, message }) => this.push(InfoModal, { title, message })),\n        ];\n    }\n\n    componentWillUnmount() {\n        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());\n    }\n\n    push(ComponentClass, componentProps) {\n        const index = this.state.components.length;\n        this.setState((state) => {\n            if (index === 0) {\n                state.sourceElement = document.activeElement;\n            }\n            state.components.push({ ComponentClass, componentProps });\n            return state;\n        });\n        return this.pop.bind(this, index);\n    }\n\n    pop(index, callback) {\n        this.setState((state) => {\n            state.components.pop();\n            assert(index === state.components.length);\n            if (index === 0) {\n                state.sourceElement.focus();\n                state.sourceElement = null;\n            }\n            return state;\n        }, callback);\n    }\n\n    renderItem({ ComponentClass, componentProps }, index) {\n        return (\n            <ComponentClass\n                key={index}\n                {...componentProps}\n                onClose={(...args) => this.pop(index, () => {\n                    if (componentProps.onClose) {\n                        componentProps.onClose(...args);\n                    }\n                })}\n            />\n        );\n    }\n\n    render() {\n        return this.state.components.map((item, index) => this.renderItem(item, index));\n    }\n}\n\nexport default ModalStack;\n"
  },
  {
    "path": "src/client/Common/Plugins.js",
    "content": "/* eslint-disable max-classes-per-file */\n\nimport React from 'react';\n\nimport { Enum } from '../../common/data_types';\nimport PropTypes from '../prop-types';\nimport SettingsContext from './SettingsContext';\n\nexport class PluginClient {\n    static getSettingsKey() {\n        // The key corresponding to the setting for your plugin.\n        // Must be unique across all plugins.\n        // Maybe infer this based on path?\n        throw new Error('not implemented');\n    }\n\n    static getSettingsComponent() {\n        // Return a React element that is rendered in the SettingsEditor.\n        // Props = { disabled: bool, value: any, onChange: function }\n        throw new Error('not implemented');\n    }\n\n    static getDisplayLocation() {\n        // A string that indicated where this component should be rendered.\n        // The various options can be found in Application.js\n        throw new Error('not implemented');\n    }\n\n    static getDisplayComponent() {\n        // Return a React element that is rendered in the application UI.\n        // Gets the \"setting\" as property.\n        throw new Error('not implemented');\n    }\n\n    static getTabData() {\n        // Return an object that contains data about an extra Tab.\n        // { value: string, label: string }\n        throw new Error('not implemented');\n    }\n}\n\nexport const PluginDisplayLocation = Enum([\n    {\n        value: 'tab_section',\n    },\n    {\n        value: 'right_sidebar_main_top',\n    },\n    {\n        value: 'right_sidebar_main_bottom',\n    },\n    {\n        value: 'right_sidebar_widgets_top',\n    },\n    {\n        value: 'right_sidebar_widgets_bottom',\n    },\n]);\n\nexport class PluginDisplayComponent extends React.Component {\n    renderActual(settings) {\n        const results = [];\n        Object.entries(this.props.plugins).forEach(([name, api]) => {\n            if (api.getDisplayLocation() !== this.props.location) {\n                return;\n            }\n            const key = api.getSettingsKey();\n            const props = {\n                settings: key ? settings[key] : null,\n            };\n            results.push(<div key={`plugin:${name}`}>{api.getDisplayComponent(props)}</div>);\n        });\n        return results;\n    }\n\n    render() {\n        return (\n            <SettingsContext.Consumer>\n                {(settings) => this.renderActual(settings)}\n            </SettingsContext.Consumer>\n        );\n    }\n}\n\nPluginDisplayComponent.propTypes = {\n    plugins: PropTypes.Custom.Plugins.isRequired,\n    location: PropTypes.string.isRequired,\n};\n"
  },
  {
    "path": "src/client/Common/PopoverElement.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport OverlayTrigger from 'react-bootstrap/OverlayTrigger';\nimport Popover from 'react-bootstrap/Popover';\nimport { MdClose } from 'react-icons/md';\n\nimport InputLine from './InputLine';\n\nclass PopoverElement extends React.Component {\n    renderOverlayTrigger() {\n        const overlay = (\n            <Popover id=\"date-range-selector\">\n                {this.props.children[1]}\n            </Popover>\n        );\n        return (\n            <OverlayTrigger\n                trigger=\"click\"\n                rootClose\n                placement=\"bottom-start\"\n                overlay={overlay}\n            >\n                <InputLine styled className=\"px-1\">\n                    {this.props.children[0]}\n                </InputLine>\n            </OverlayTrigger>\n        );\n    }\n\n    renderButton() {\n        return <Button onClick={() => this.props.onReset(null)}><MdClose /></Button>;\n    }\n\n    render() {\n        return (\n            <>\n                {this.renderOverlayTrigger()}\n                {this.renderButton()}\n            </>\n        );\n    }\n}\n\nPopoverElement.propTypes = {\n    onReset: PropTypes.func.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any.isRequired,\n};\n\nexport default PopoverElement;\n"
  },
  {
    "path": "src/client/Common/ScrollableSection.css",
    "content": ".scrollable-section {\n    padding-right: 4px;\n    overflow-y: scroll;\n}\n\n.scrollable-section::-webkit-scrollbar {\n    width: 8px;\n}\n\n.scrollable-section::-webkit-scrollbar-track {\n    background: var(--background-color);\n    border-radius: 4px;\n}\n\n.scrollable-section::-webkit-scrollbar-thumb {\n    background-color: var(--component-color);\n    border-radius: 4px;\n}\n"
  },
  {
    "path": "src/client/Common/ScrollableSection.js",
    "content": "/* eslint-disable max-classes-per-file */\n\nimport './ScrollableSection.css';\n\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nclass WindowHeightDetector {\n    static subscribe(callback) {\n        if (!WindowHeightDetector.instance) {\n            WindowHeightDetector.instance = new WindowHeightDetector();\n        }\n        const { instance } = WindowHeightDetector;\n        instance.callbacks.push(callback);\n        return instance.height;\n    }\n\n    constructor() {\n        this.callbacks = [];\n        this.height = window.innerHeight;\n        window.addEventListener('resize', this.onResize.bind(this));\n    }\n\n    onResize() {\n        this.height = window.innerHeight;\n        this.callbacks.forEach((callback) => callback(this.height));\n    }\n}\n\nclass ScrollableSection extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            height: WindowHeightDetector.subscribe((height) => this.setState({ height })),\n        };\n    }\n\n    render() {\n        const height = this.state.height\n            - this.props.padding\n            - 32; // Why 32? 16px padding at top/bottom.\n        return (\n            <div className=\"scrollable-section\" style={{ height }}>\n                {this.props.children}\n            </div>\n        );\n    }\n}\n\nScrollableSection.propTypes = {\n    padding: PropTypes.number,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nScrollableSection.defaultProps = {\n    padding: 0,\n};\n\nexport default ScrollableSection;\n"
  },
  {
    "path": "src/client/Common/Selector.js",
    "content": "/* eslint-disable max-classes-per-file */\n\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\n\nclass Selector extends React.Component {\n    constructor(props) {\n        super(props);\n        this.ref = React.createRef();\n    }\n\n    focus() {\n        this.ref.current.focus();\n    }\n\n    render() {\n        const { onChange, options, ...moreProps } = this.props;\n        return (\n            <Form.Control\n                {...moreProps}\n                className=\"selector\"\n                as=\"select\"\n                onChange={(event) => onChange(event.target.value)}\n                ref={this.ref}\n            >\n                {options.map((item) => {\n                    const optionProps = { key: item.value, value: item.value };\n                    return <option {...optionProps}>{item.label}</option>;\n                })}\n            </Form.Control>\n        );\n    }\n}\n\nSelector.propTypes = {\n    value: PropTypes.string.isRequired,\n    options: PropTypes.arrayOf(\n        PropTypes.shape({\n            label: PropTypes.string.isRequired,\n            value: PropTypes.string.isRequired,\n        }),\n    ).isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nSelector.getStringListOptions = (items) => items.map((item) => ({\n    label: item,\n    value: item,\n}));\n\nclass BinarySelector extends React.Component {\n    constructor(props) {\n        super(props);\n        this.ref = React.createRef();\n    }\n\n    focus() {\n        this.ref.current.focus();\n    }\n\n    render() {\n        const {\n            noLabel, yesLabel, value, onChange, ...moreProps\n        } = this.props;\n        const options = [\n            { label: noLabel, value: 'no' },\n            { label: yesLabel, value: 'yes' },\n        ];\n        return (\n            <Selector\n                {...moreProps}\n                value={options[value ? 1 : 0].value}\n                options={options}\n                onChange={(newValue) => onChange(newValue === options[1].value)}\n                ref={this.ref}\n            />\n        );\n    }\n}\n\nBinarySelector.propTypes = {\n    value: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n    noLabel: PropTypes.string,\n    yesLabel: PropTypes.string,\n};\n\nBinarySelector.defaultProps = {\n    noLabel: 'No',\n    yesLabel: 'Yes',\n};\n\nSelector.Binary = BinarySelector;\n\nexport default Selector;\n"
  },
  {
    "path": "src/client/Common/SettingsContext.js",
    "content": "import React from 'react';\n\nconst SettingsContext = React.createContext({});\n\nSettingsContext.Wrapper = (Component) => (moreProps) => (\n    <SettingsContext.Consumer>\n        {(settings) => <Component settings={settings} {...moreProps} />}\n    </SettingsContext.Consumer>\n);\n\nexport default SettingsContext;\n"
  },
  {
    "path": "src/client/Common/SidebarSection.css",
    "content": ".sidebar-section {\n    border: 1px solid var(--background-color);\n    border-radius: 4px;\n    padding: 3px 8px 5px;\n    margin: 4px 0;\n}\n\n.sidebar-section:hover {\n    border-color: var(--component-highlight-color);\n}\n\n.sidebar-section.selected {\n    background: var(--component-color);\n    border-color: var(--component-highlight-color);\n}\n\n.sidebar-section > .header {\n    color: var(--text-disabled-color);\n}\n\n.sidebar-section > .cursor {\n    cursor: pointer;\n}\n\n.sidebar-section > .separator {\n    border-bottom: 1px solid var(--component-highlight-color);\n    margin-bottom: 4px;\n    padding-bottom: 4px;\n}\n\n.sidebar-section > li {\n    width: 1000px;\n}\n\n.sidebar-section > li > a {\n    left: -8px;\n    position: relative;\n}\n"
  },
  {
    "path": "src/client/Common/SidebarSection.js",
    "content": "import './SidebarSection.css';\n\nimport classNames from 'classnames';\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { GoPrimitiveDot } from 'react-icons/go';\nimport { TiMinus, TiPlus } from 'react-icons/ti';\n\nimport Icon from './Icon';\nimport LeftRight from './LeftRight';\n\nclass SidebarSection extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { isCollapsed: false };\n    }\n\n    renderHeader() {\n        if (!this.props.title) {\n            return null;\n        }\n        const { isCollapsed } = this.state;\n        return (\n            <LeftRight\n                className={classNames({\n                    header: true,\n                    cursor: true,\n                    separator: !isCollapsed,\n                })}\n                onClick={() => this.setState({ isCollapsed: !isCollapsed })}\n            >\n                {this.props.title}\n                <Icon>{isCollapsed ? <TiPlus /> : <TiMinus />}</Icon>\n            </LeftRight>\n        );\n    }\n\n    renderChildren() {\n        if (this.state.isCollapsed) {\n            return null;\n        }\n        return (\n            <div className={classNames({ cursor: !this.props.title })}>\n                {this.props.children}\n            </div>\n        );\n    }\n\n    render() {\n        const {\n            selected, title: _title, children: _children, ...moreProps\n        } = this.props;\n        return (\n            <div\n                {...moreProps}\n                className={classNames({\n                    'sidebar-section': true,\n                    selected,\n                })}\n            >\n                {this.renderHeader()}\n                {this.renderChildren()}\n            </div>\n        );\n    }\n}\n\nSidebarSection.propTypes = {\n    selected: PropTypes.bool,\n    // eslint-disable-next-line react/forbid-prop-types\n    title: PropTypes.any,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nSidebarSection.Item = function ({ children }) {\n    return (\n        <InputGroup>\n            <Icon alwaysHighlighted className=\"mr-1\">\n                <GoPrimitiveDot />\n            </Icon>\n            {children}\n        </InputGroup>\n    );\n};\n\nSidebarSection.Item.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nexport default SidebarSection;\n"
  },
  {
    "path": "src/client/Common/SortableList.css",
    "content": "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",
    "content": "import './SortableList.css';\n\nimport arrayMove from 'array-move';\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport { GoTrashcan } from 'react-icons/go';\nimport { GrDrag } from 'react-icons/gr';\nimport { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';\n\nconst SortableDragHandle = SortableHandle((props) => (\n    <Button\n        className=\"sortableDragHandle\"\n        disabled={props.disabled}\n    >\n        <GrDrag />\n    </Button>\n));\n\nconst WrappedContainer = SortableContainer(({ children }) => <div>{children}</div>);\n\nconst WrappedRow = SortableElement((props) => {\n    const disabled = props.wrappedRowDisabled;\n    const { children, ...otherProps } = props.originalElement.props;\n    return React.createElement(\n        props.originalElement.type,\n        otherProps,\n        [\n            <SortableDragHandle\n                key=\"drag\"\n                disabled={disabled}\n                title=\"Reorder\"\n            />,\n            ...(children || []),\n            <Button\n                key=\"delete\"\n                disabled={disabled}\n                onClick={props.onDelete}\n                title=\"Delete\"\n            >\n                <GoTrashcan />\n            </Button>,\n        ],\n    );\n});\n\nclass SortableList extends React.Component {\n    constructor(props) {\n        super(props);\n        let { type } = props;\n        if (type.constructor) {\n            type = (innerProps) => React.createElement(props.type, innerProps);\n        }\n        this.state = { type };\n    }\n\n    onReorder({ oldIndex, newIndex }) {\n        this.props.onChange(arrayMove(this.props.items, oldIndex, newIndex));\n    }\n\n    onChange(index, item) {\n        const items = [...this.props.items];\n        items[index] = item;\n        this.props.onChange(items);\n    }\n\n    onDelete(index) {\n        const items = [...this.props.items];\n        items.splice(index, 1);\n        this.props.onChange(items);\n    }\n\n    renderRow(item, index) {\n        const {\n            items, itemsKey, onChange: _onChange, type: _type, disabled, ...moreProps\n        } = this.props;\n        return React.createElement(WrappedRow, {\n            key: item.__id__,\n            // Consumed by SortableElement\n            index,\n            disabled,\n            // Forwarded to the WrappedRow.\n            originalElement: this.state.type({\n                disabled,\n                onChange: (updatedItem) => this.onChange(index, updatedItem),\n                [itemsKey]: items,\n                index,\n                ...moreProps,\n            }),\n            wrappedRowDisabled: disabled,\n            onDelete: () => this.onDelete(index),\n        });\n    }\n\n    render() {\n        return (\n            <WrappedContainer\n                helperClass=\"sortableDraggedItem\"\n                useDragHandle\n                onSortEnd={(data) => this.onReorder(data)}\n            >\n                {this.props.items.map((item, index) => this.renderRow(item, index))}\n            </WrappedContainer>\n        );\n    }\n}\n\nSortableList.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    items: PropTypes.arrayOf(PropTypes.any.isRequired).isRequired,\n    onChange: PropTypes.func.isRequired,\n    type: PropTypes.func.isRequired,\n    itemsKey: PropTypes.string.isRequired,\n    disabled: PropTypes.bool.isRequired,\n};\n\nexport default SortableList;\n"
  },
  {
    "path": "src/client/Common/StandardIcons.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport { AiOutlineWarning } from 'react-icons/ai';\nimport { BiDetail } from 'react-icons/bi';\nimport { MdHelp, MdInfo } from 'react-icons/md';\n\nfunction getIconWrapper(Component, color = 'var(--link-color)', style = { cursor: 'pointer' }) {\n    function IconWrapper(props) {\n        const { isShown, ...moreProps } = props;\n        if (!isShown) {\n            return null;\n        }\n        return (\n            <Component\n                className=\"ml-1\"\n                color={color}\n                style={style}\n                {...moreProps}\n            />\n        );\n    }\n    IconWrapper.propTypes = {\n        isShown: PropTypes.bool.isRequired,\n    };\n    return IconWrapper;\n}\n\nexport const WarningIcon = getIconWrapper(AiOutlineWarning, 'var(--warning-color)', { position: 'relative', top: -1 });\nexport const DetailsIcon = getIconWrapper(BiDetail);\nexport const HelpIcon = getIconWrapper(MdHelp);\nexport const InfoIcon = getIconWrapper(MdInfo);\n"
  },
  {
    "path": "src/client/Common/TextEditor.css",
    "content": ".public-DraftStyleDefault-ul,\n.public-DraftStyleDefault-ol {\n    margin: 0;\n}\n\n.text-editor {\n    position: relative;\n}\n\n.text-editor.min-width .public-DraftEditor-content {\n    min-width: 100px;\n}\n\n.text-editor.styled {\n    background: var(--input-background-color);\n    color: var(--input-text-color);\n}\n\n.text-editor.styled .public-DraftEditor-content {\n    background: var(--input-background-color);\n    border-radius: 0 0.2rem 0.2rem 0;\n    color: var(--input-text);\n    min-height: 20px;\n    padding: 1px 4px 0;\n}\n\n.text-editor.styled.disabled .public-DraftEditor-content {\n    background: var(--input-disabled-background-color);\n}\n\n.text-editor.isSingleLine {\n    display: inline-block;\n}\n\n/* TextEditor Mention Suggestions + Dropdown Options (used by RBT Suggestions) */\n\n.dropdown-menu,\n.text-editor .mention-suggestions > div {\n    background-color: var(--component-color);\n    border: 1px solid var(--component-highlight-color);\n    box-shadow: 0px 4px 30px 0px var(--component-color);\n    font-family: var(--font-family);\n    font-size: var(--font-size);\n    min-width: 100px;\n    position: absolute;\n    z-index: 2;\n}\n\n.dropdown-item,\n.text-editor .mention-suggestions > div > div {\n    color: var(--text-color);\n    padding: 1px 16px;\n}\n\n.text-editor .mention-suggestions > div > div > span {\n    font-size: inherit;\n    margin: 0;\n    overflow: inherit;\n}\n\n.dropdown-item.active,\n.text-editor .mention-suggestions > div > div[aria-selected=\"true\"] {\n    background-color: var(--suggestion-highlight-color);\n}\n"
  },
  {
    "path": "src/client/Common/TextEditor.js",
    "content": "import 'draft-js/dist/Draft.css';\nimport './TextEditor.css';\n\nimport assert from 'assert';\nimport classNames from 'classnames';\nimport { RichUtils } from 'draft-js';\nimport createMarkdownShortcutsPlugin from 'draft-js-markdown-shortcuts-plugin';\nimport createMentionPlugin from 'draft-js-mention-plugin';\nimport Editor from 'draft-js-plugins-editor';\nimport PropTypes from 'prop-types';\nimport React from 'react';\n\nimport RichTextUtils from '../../common/RichTextUtils';\nimport AddLinkPlugin from './AddLinkPlugin';\nimport Link from './Link';\nimport TypeaheadOptions from './TypeaheadOptions';\nimport { KeyCodes } from './Utils';\n\nfunction MentionComponent(props) {\n    return <Link logTopic={props.mention}>{props.children}</Link>;\n}\n\nMentionComponent.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    mention: PropTypes.any.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any,\n};\n\nfunction OptionComponent(props) {\n    const {\n        isFocused: _isFocused, // eslint-disable-line react/prop-types\n        mention: item,\n        searchValue: _searchValue, // eslint-disable-line react/prop-types\n        theme: _theme, // eslint-disable-line react/prop-types\n        ...moreProps\n    } = props;\n    return <div {...moreProps}>{item.name}</div>;\n}\n\nOptionComponent.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    mention: PropTypes.any.isRequired,\n};\n\nclass TextEditor extends React.Component {\n    static getDerivedStateFromProps(props, state) {\n        if (state.onChange) {\n            return { onChange: false };\n        }\n        const isFirstTime = !('value' in state);\n        // WARNING: Even if props.value is equivalent to state.value, they might\n        // not be in the same format, and that could lead to an infinite loop!\n        if (isFirstTime || !RichTextUtils.equals(state.value, props.value)) {\n            return {\n                value: props.value,\n                editorState: RichTextUtils.toEditorState(props.value),\n            };\n        }\n        return null;\n    }\n\n    constructor(props) {\n        super(props);\n        this.state = {\n            suggestions: [],\n            open: false,\n            plugins: [],\n        };\n\n        this.state.plugins.push(AddLinkPlugin);\n\n        if (!this.props.isSingleLine) {\n            this.markdownShortcutsPlugin = createMarkdownShortcutsPlugin();\n            this.state.plugins.push(this.markdownShortcutsPlugin);\n        }\n\n        this.mentionPlugin = createMentionPlugin({\n            mentionComponent: MentionComponent,\n            supportWhitespace: true,\n        });\n        // Workaround for two bugs in draft-js-mention-plugin v3.x:\n        // 1. Deleting @ doesn't close the dropdown (early return skips closeDropdown).\n        // 2. Typing @ at a non-zero offset doesn't trigger search (> vs >=).\n        // Detection: onSearchChange fires synchronously inside pluginOnChange\n        // when the plugin finds an active search. If it didn't fire, either\n        // the search ended (clear suggestions) or was skipped (trigger manually).\n        const pluginOnChange = this.mentionPlugin.onChange;\n        this.mentionPlugin.onChange = (editorState) => {\n            this._searchFired = false;\n            const result = pluginOnChange(editorState);\n            if (!this._searchFired) {\n                if (this.state.open) {\n                    this.setState({ suggestions: [] });\n                } else if (this.props.options) {\n                    const text = editorState.getCurrentContent().getPlainText();\n                    const offset = editorState.getSelection().getAnchorOffset();\n                    if (offset > 0 && text.charAt(offset - 1) === '@') {\n                        this.onSearchChange({ value: '' });\n                    }\n                }\n            }\n            return result;\n        };\n        this.state.plugins.push(this.mentionPlugin);\n\n        this.ref = React.createRef();\n    }\n\n    handleKeyCommand(command, editorState) {\n        const newState = RichUtils.handleKeyCommand(editorState, command);\n        if (newState) {\n            this.onChange(newState);\n            return 'handled';\n        }\n        if (this.props.isSingleLine && command === 'split-block') {\n            return 'handled';\n        }\n        return 'not-handled';\n    }\n\n    onSearchChange({ value: query }) {\n        this._searchFired = true;\n        this.props.options\n            .search(query)\n            .then((suggestions) => this.setState({ suggestions }));\n    }\n\n    onAddMention(option) {\n        this.props.options\n            .select(option)\n            .then((result) => {\n                if (typeof result === 'undefined') return;\n                const selection = RichTextUtils.getSelectionData(this.state.editorState);\n                // Abstraction leak! Do not assume name.\n                const delta = result.name.length - option.name.length;\n                selection.anchorOffset += delta;\n                selection.focusOffset += delta;\n                let content = RichTextUtils.fromEditorState(this.state.editorState);\n                content = RichTextUtils.updateDraftContent(content, [option], [result || '']);\n                let editorState = RichTextUtils.toEditorState(content);\n                // TODO: Figure out why the cursor is not updated properly.\n                editorState = RichTextUtils.setSelectionData(editorState, selection);\n                this.onChange(editorState);\n            });\n    }\n\n    onChange(editorState) {\n        editorState = RichTextUtils.fixCursorBug(this.state.editorState, editorState);\n        this.setState({ editorState });\n        const newValue = RichTextUtils.fromEditorState(editorState);\n        if (this.props.onChange) {\n            this.setState(\n                { onChange: true, value: newValue },\n                () => this.props.onChange(newValue),\n            );\n        }\n    }\n\n    focus() {\n        // Why the delay?\n        // This broke something inside the DraftJS Editor\n        // that caused mentions to not be rendered properly.\n        window.setTimeout(this.ref.current.focus, 0);\n    }\n\n    keyBindingFn(event) {\n        if (\n            this.props.isSingleLine\n            && [KeyCodes.ESCAPE, KeyCodes.ENTER].includes(event.keyCode)\n            && this.props.onSpecialKeys\n        ) {\n            this.props.onSpecialKeys(event);\n        }\n        // https://github.com/draft-js-plugins/draft-js-plugins/issues/1117\n        // Do not invoke getDefaultKeyBinding here!\n    }\n\n    renderSuggestions() {\n        const { MentionSuggestions } = this.mentionPlugin;\n        const suggestions = this.state.suggestions\n            .map((item) => ({ id: `${item.__type__}:${item.__id__}`, ...item }));\n        return (\n            <div className=\"mention-suggestions\">\n                <MentionSuggestions\n                    onOpen={() => this.setState({ open: true })}\n                    onClose={() => this.setState({ open: false })}\n                    onSearchChange={(data) => this.onSearchChange(data)}\n                    onAddMention={(option) => this.onAddMention(option)}\n                    suggestions={suggestions}\n                    entryComponent={OptionComponent}\n                />\n            </div>\n        );\n    }\n\n    render() {\n        return (\n            <div className={classNames({\n                'text-editor': true,\n                unstyled: this.props.unstyled,\n                styled: !this.props.unstyled,\n                disabled: this.props.disabled,\n                isSingleLine: this.props.isSingleLine,\n                'min-width': this.props.minWidth,\n            })}\n            >\n                <Editor\n                    readOnly={this.props.disabled}\n                    editorState={this.state.editorState}\n                    keyBindingFn={(event) => this.keyBindingFn(event)}\n                    handleKeyCommand={\n                        (command, editorState) => this.handleKeyCommand(command, editorState)\n                    }\n                    plugins={this.state.plugins}\n                    onChange={(editorState) => this.onChange(editorState)}\n                    placeholder={this.props.placeholder}\n                    ref={this.ref}\n                />\n                {this.props.disabled ? null : this.renderSuggestions()}\n            </div>\n        );\n    }\n}\n\nTextEditor.propTypes = {\n    unstyled: PropTypes.bool,\n    disabled: PropTypes.bool,\n    placeholder: PropTypes.string,\n    minWidth: PropTypes.bool,\n\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.object,\n    onChange: PropTypes.func,\n\n    isSingleLine: PropTypes.bool,\n    onSpecialKeys: PropTypes.func,\n\n    options: PropTypes.instanceOf(TypeaheadOptions),\n};\n\nTextEditor.defaultProps = {\n    unstyled: false,\n    disabled: false,\n    isSingleLine: false,\n    minWidth: false,\n};\n\n/**\n * The primary component generates an inline block that does not look great on\n * the main page. This alternative implementation generates an inline span.\n */\nTextEditor.SimpleViewer = function (props) {\n    if (!props.value) {\n        return null;\n    }\n    const rawContent = props.value;\n    assert(rawContent.blocks.length === 1);\n    const { text } = rawContent.blocks[0];\n    let textIndex = 0;\n    const entityRanges = Object.values(rawContent.blocks[0].entityRanges);\n    let entityRangeIndex = 0;\n    const parts = [];\n    while (textIndex < text.length) {\n        const entityRange = entityRanges[entityRangeIndex];\n        let part;\n        if (entityRange && entityRange.offset === textIndex) {\n            const key = `entity-${entityRangeIndex}`;\n            entityRangeIndex += 1;\n            const entity = rawContent.entityMap[entityRange.key];\n            const endIndex = textIndex + entityRange.length;\n            const textPart = text.slice(textIndex, endIndex);\n            textIndex = endIndex;\n            if (entity.type === 'mention') {\n                part = (\n                    <Link key={key} logTopic={entity.data.mention}>\n                        {textPart}\n                    </Link>\n                );\n            } else if (entity.type === 'LINK') {\n                part = (\n                    <a key={key} href={entity.data.url} target=\"_blank\" rel=\"noopener noreferrer\">\n                        {textPart}\n                    </a>\n                );\n            } else {\n                assert(false, `unknown entity type: ${entity.type}`);\n            }\n        } else {\n            const endIndex = entityRange ? entityRange.offset : text.length;\n            const textPart = text.slice(textIndex, endIndex);\n            textIndex = endIndex;\n            part = textPart;\n        }\n        parts.push(part);\n    }\n    return <span>{parts}</span>;\n};\n\nTextEditor.SimpleViewer.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.any,\n};\n\nexport default TextEditor;\n"
  },
  {
    "path": "src/client/Common/TextInput.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\n\nclass TextInput extends React.Component {\n    constructor(props) {\n        super(props);\n        this.ref = React.createRef();\n    }\n\n    focus() {\n        this.ref.current.focus();\n    }\n\n    render() {\n        const {\n            value, disabled, onChange, ...moreProps\n        } = this.props;\n        return (\n            <Form.Control\n                value={value}\n                disabled={disabled}\n                onChange={(event) => onChange(event.target.value)}\n                ref={this.ref}\n                {...moreProps}\n            />\n        );\n    }\n}\n\nTextInput.propTypes = {\n    value: PropTypes.string.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default TextInput;\n"
  },
  {
    "path": "src/client/Common/TooltipElement.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport OverlayTrigger from 'react-bootstrap/OverlayTrigger';\nimport Tooltip from 'react-bootstrap/Tooltip';\n\nfunction TooltipElement(props) {\n    const overlay = (\n        <Tooltip style={{ width: 200 }}>\n            {props.children[1]}\n        </Tooltip>\n    );\n    return (\n        <OverlayTrigger\n            rootClose\n            placement=\"right-start\"\n            overlay={overlay}\n        >\n            {props.children[0]}\n        </OverlayTrigger>\n    );\n}\n\nTooltipElement.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    children: PropTypes.any.isRequired,\n};\n\nexport default TooltipElement;\n"
  },
  {
    "path": "src/client/Common/TypeaheadInput.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport { AsyncTypeahead } from 'react-bootstrap-typeahead';\n\nclass TypeaheadInput extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { isLoading: false, options: [] };\n        this.ref = React.createRef();\n    }\n\n    onSearch(query) {\n        this.setState({ isLoading: true }, () => {\n            this.props.onSearch(query)\n                .then((options) => this.setState({ isLoading: false, options }));\n        });\n    }\n\n    focus() {\n        this.ref.current.focus();\n    }\n\n    render() {\n        return (\n            <AsyncTypeahead\n                id={this.props.id}\n                minLength={0}\n                disabled={this.props.disabled}\n                onFocus={() => this.onSearch(this.props.value)}\n                onSearch={(query) => this.onSearch(query)}\n                filterBy={this.props.filterBy}\n                placeholder={this.props.placeholder}\n                selected={[this.props.value]}\n                onInputChange={(newValue) => {\n                    this.onSearch(newValue);\n                    this.props.onChange(newValue);\n                }}\n                onChange={(newSelected) => {\n                    if (newSelected.length) {\n                        this.props.onChange(newSelected[0]);\n                    }\n                }}\n                renderMenuItemChildren={(option) => <div>{option}</div>}\n                isLoading={this.state.isLoading}\n                options={this.state.options}\n                ref={this.ref}\n            />\n        );\n    }\n}\n\nTypeaheadInput.propTypes = {\n    id: PropTypes.string.isRequired,\n    value: PropTypes.string.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n    onSearch: PropTypes.func.isRequired,\n\n    placeholder: PropTypes.string,\n    filterBy: PropTypes.func,\n};\n\nexport default TypeaheadInput;\n"
  },
  {
    "path": "src/client/Common/TypeaheadOptions.js",
    "content": "import assert from 'assert';\n\nclass TypeaheadOptions {\n    static getFromTypes(names) {\n        return new TypeaheadOptions({\n            serverSideOptions: names.map((name) => ({ name })),\n        });\n    }\n\n    constructor(config) {\n        assert(Array.isArray(config.serverSideOptions));\n        if (!config.prefixOptions) {\n            config.prefixOptions = [];\n        }\n        if (!config.suffixOptions) {\n            config.suffixOptions = [];\n        }\n        if (!config.getComputedOptions) {\n            config.getComputedOptions = async () => [];\n        }\n        if (!config.computedOptionTypes) {\n            config.computedOptionTypes = [];\n        }\n        if (!config.allowMultipleItems) {\n            config.allowMultipleItems = {};\n            config.allowMultipleItems['log-topic'] = true;\n        }\n        this.config = config;\n    }\n\n    async search(query, existingItems) {\n        const skipTypes = {};\n        if (existingItems) {\n            // When existing items are provided, we can check to see which types\n            // have already been selected, and exclude them from the results.\n            existingItems.forEach((item) => {\n                if (!this.config.allowMultipleItems[item.__type__]) {\n                    skipTypes[item.__type__] = true;\n                }\n            });\n        }\n        // Server-side filtering invokes case insensitive LIKE `${query}%`.\n        let options = await Promise.all(\n            this.config.serverSideOptions\n                .filter((item) => !skipTypes[item.name])\n                .map((item) => window.api.send(\n                    `${item.name}-typeahead`,\n                    { query, where: item.where },\n                )),\n        );\n        options = options.flat();\n\n        const doesMatchQuery = (item) => item.name.toLowerCase().startsWith(query.toLowerCase());\n        options = [\n            ...this.config.prefixOptions\n                .filter((item) => !skipTypes[item.__type__])\n                .filter(doesMatchQuery),\n            ...options,\n            ...this.config.suffixOptions\n                .filter((item) => !skipTypes[item.__type__])\n                .filter(doesMatchQuery),\n        ];\n        if (this.config.serverSideOptions.length > 1) {\n            const seenOptionIds = new Set();\n            // Since option.__id__ is used as a React Array Key, adjust it.\n            // Do this only if needed to minimize later adjustment.\n            options.forEach((option) => {\n                if (seenOptionIds.has(option.__id__)) {\n                    option.__original_id__ = option.__id__;\n                    option.__id__ = `${option.__type__}:${option.__id__}`;\n                } else {\n                    seenOptionIds.add(option.__id__);\n                }\n            });\n        }\n        const computedOptions = await this.config.getComputedOptions(query);\n        options.push(...computedOptions);\n        // TODO: Maybe prefix type name, before item name, for clarity.\n        return options;\n    }\n\n    async select(option) {\n        let adjusted = false;\n        if (option.__original_id__) {\n            option.__id__ = option.__original_id__;\n            delete option.__original_id__;\n            adjusted = true;\n        }\n        if (this.config.onSelect) {\n            const result = await this.config.onSelect(option);\n            // undefined = no change\n            // null = cancel operation\n            if (typeof result === 'object') {\n                option = result;\n                adjusted = true;\n            }\n        }\n        return adjusted ? option : undefined;\n    }\n\n    // This method is used while switching between different tabs,\n    // in an attempt to retain as many search filters as possible.\n    filterToKnownTypes(items) {\n        const knownTypes = new Set([\n            ...this.config.serverSideOptions.map((option) => option.name),\n            ...this.config.prefixOptions.map((option) => option.__type__),\n            ...this.config.suffixOptions.map((option) => option.__type__),\n            ...this.config.computedOptionTypes,\n        ]);\n        return items.filter((item) => knownTypes.has(item.__type__));\n    }\n}\n\nexport default TypeaheadOptions;\n"
  },
  {
    "path": "src/client/Common/TypeaheadSelector.css",
    "content": ".rbt input:first-child {\n    background-color: var(--input-background-color);\n    color: var(--input-text-color);\n    font-size: var(--font-size);\n}\n\n.rbt .rbt-input-multi {\n    background-color: var(--input-background-color);\n    border: none;\n    border-radius: 0;\n    padding: 1px 2px;\n}\n\n.rbt .rbt-input-multi .rbt-token {\n    background-color: var(--input-background-token-color);\n    color: var(--input-text-color);\n    font-size: var(--font-size);\n    margin: 3px 1px 0;\n    padding-bottom: 1px;\n    padding-top: 1px;\n}\n\n.rbt .rbt-input-multi input:first-child {\n    margin-bottom: 2px;\n    margin-left: 2px;\n}\n"
  },
  {
    "path": "src/client/Common/TypeaheadSelector.js",
    "content": "import 'react-bootstrap-typeahead/css/Typeahead.min.css';\nimport './TypeaheadSelector.css';\n\nimport PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport { AsyncTypeahead } from 'react-bootstrap-typeahead';\nimport { MdClose } from 'react-icons/md';\n\nimport TypeaheadOptions from './TypeaheadOptions';\n\nclass TypeaheadSelector extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { isLoading: false, text: '', options: [] };\n        this.ref = React.createRef();\n    }\n\n    onInputChange(text) {\n        this.setState({ text });\n        this.onSearch(text);\n    }\n\n    onSearch(query) {\n        this.setState({ isLoading: true });\n        let existingItems = [];\n        if (this.props.multiple) {\n            existingItems = this.props.value;\n        } else {\n            existingItems = (this.props.value ? [this.props.value] : []);\n        }\n        this.props.options\n            .search(query, existingItems)\n            .then((options) => this.setState({ isLoading: false, options }, this.forceUpdate));\n    }\n\n    async onChange(selected) {\n        if (selected.length) {\n            const index = selected.length - 1;\n            const result = await this.props.options.select(selected[index]);\n            if (result) {\n                selected[index] = result;\n            } else if (result === null) {\n                return;\n            }\n        }\n        if (this.props.multiple) {\n            this.props.onChange(selected);\n        } else {\n            this.props.onChange(selected[0] || null);\n        }\n    }\n\n    focus() {\n        this.ref.current.focus();\n    }\n\n    renderDeleteButton() {\n        if (this.props.disabled || !this.props.value) {\n            return null;\n        }\n        return (\n            <Button\n                onClick={() => this.props.onChange(null)}\n                title=\"Cancel\"\n            >\n                <MdClose style={{ fill: 'white !important' }} />\n            </Button>\n        );\n    }\n\n    render() {\n        const commonProps = {\n            ...this.state,\n            id: this.props.id,\n            labelKey: 'name',\n            minLength: 0,\n            onFocus: () => this.onSearch(this.state.text),\n            onSearch: (query) => this.onSearch(query),\n            placeholder: this.props.placeholder,\n            onInputChange: (text) => this.onInputChange(text),\n            onChange: (selected) => this.onChange(selected),\n            filterBy: (option, props) => true,\n            renderMenuItemChildren: (option) => <div>{option.name}</div>,\n            ref: this.ref,\n        };\n        if (this.props.multiple) {\n            return (\n                <AsyncTypeahead\n                    {...commonProps}\n                    multiple\n                    disabled={this.props.disabled}\n                    selected={this.props.value}\n                />\n            );\n        }\n        return (\n            <>\n                <AsyncTypeahead\n                    {...commonProps}\n                    disabled={this.props.disabled || this.props.value}\n                    selected={this.props.value ? [this.props.value] : []}\n                />\n                {this.renderDeleteButton()}\n            </>\n        );\n    }\n}\n\nTypeaheadSelector.propTypes = {\n    id: PropTypes.string.isRequired,\n    multiple: PropTypes.bool,\n    options: PropTypes.instanceOf(TypeaheadOptions).isRequired,\n\n    value: PropTypes.oneOfType([\n        // eslint-disable-next-line react/no-typos\n        PropTypes.Custom.Item,\n        PropTypes.arrayOf(PropTypes.Custom.Item),\n    ]),\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n\n    placeholder: PropTypes.string,\n};\n\nTypeaheadSelector.defaultProps = {\n    multiple: false,\n};\n\nTypeaheadSelector.getStringItem = (value, index = -1) => ({\n    __type__: 'string',\n    __id__: index + 1,\n    name: value,\n});\n\nTypeaheadSelector.getStringListItems = (values) => {\n    if (!values) return [];\n    return values.map(TypeaheadSelector.getStringItem);\n};\n\nTypeaheadSelector.getStringListTypeaheadOptions = (fetcher) => new TypeaheadOptions({\n    serverSideOptions: [],\n    getComputedOptions: async (query) => {\n        // Maybe skip fetching results if query is empty?\n        let options = [];\n        if (fetcher) {\n            options = await fetcher(query);\n        }\n        options = TypeaheadSelector.getStringListItems(options);\n        options.push(TypeaheadSelector.getStringItem(query));\n        return options;\n    },\n});\n\nexport default TypeaheadSelector;\n"
  },
  {
    "path": "src/client/Common/URLManager.js",
    "content": "import assert from 'assert';\nimport queryString from 'query-string';\n\nlet onChange;\nlet pushState;\n\nconst options = { arrayFormat: 'bracket' };\n\nclass URLManager {\n    static init(callback) {\n        assert(!onChange, 'URLManager already initialized');\n        onChange = callback;\n        pushState = window.history.pushState;\n        window.history.pushState = (...args) => {\n            pushState.apply(window.history, args);\n            onChange();\n        };\n        return () => {\n            window.history.pushState = pushState;\n            pushState = null;\n            onChange = null;\n        };\n    }\n\n    static get() {\n        return queryString.parse(window.location.search, options);\n    }\n\n    static getLink(params) {\n        return `?${queryString.stringify(params, options).replace(/%20/g, '+')}`;\n    }\n\n    static update(link) {\n        window.history.pushState({}, '', link);\n    }\n}\n\nexport default URLManager;\n"
  },
  {
    "path": "src/client/Common/Utils.js",
    "content": "export function suppressUnlessShiftKey(event) {\n    if (!event.shiftKey) {\n        event.preventDefault();\n    }\n}\n\n// https://davidwalsh.name/javascript-debounce-function\nexport function debounce(func, wait, immediate) {\n    let timeout;\n    return function inner(...args) {\n        const context = this;\n        const later = () => {\n            timeout = null;\n            if (!immediate) func.apply(context, args);\n        };\n        const callNow = immediate && !timeout;\n        clearTimeout(timeout);\n        timeout = setTimeout(later, wait);\n        if (callNow) func.apply(context, args);\n    };\n}\n\nexport const KeyCodes = {\n    DELETE: 8,\n    ENTER: 13,\n    ESCAPE: 27,\n    SPACE: 32,\n    UP_ARROW: 38,\n    DOWN_ARROW: 40,\n};\n"
  },
  {
    "path": "src/client/Common/index.js",
    "content": "export { default as AsyncSelector } from './AsyncSelector';\nexport { default as BulletList } from './BulletList';\nexport { default as Coordinator } from './Coordinator';\nexport { default as DataLoader } from './DataLoader';\nexport { default as DateContext } from './DateContext';\nexport { default as DatePicker } from './DatePicker';\nexport { default as DateRangePicker } from './DateRangePicker';\nexport { default as Dropdown } from './Dropdown';\nexport { default as EditorModal } from './EditorModal';\nexport { default as EnumSelectorSection } from './EnumSelectorSection';\nexport { default as ErrorModal } from './ErrorModal';\nexport { default as Highlightable } from './Highlightable';\nexport { default as Icon } from './Icon';\nexport { default as InfoModal } from './InfoModal';\nexport { default as InputLine } from './InputLine';\nexport { default as LeftRight } from './LeftRight';\nexport { default as Link } from './Link';\nexport { default as ModalStack } from './ModalStack';\nexport { default as ScrollableSection } from './ScrollableSection';\nexport { default as Selector } from './Selector';\nexport { default as SettingsContext } from './SettingsContext';\nexport { default as SidebarSection } from './SidebarSection';\nexport { default as SortableList } from './SortableList';\nexport { default as TextInput } from './TextInput';\nexport { default as TextEditor } from './TextEditor';\nexport { default as TooltipElement } from './TooltipElement';\nexport { default as TypeaheadInput } from './TypeaheadInput';\nexport { default as TypeaheadOptions } from './TypeaheadOptions';\nexport { default as TypeaheadSelector } from './TypeaheadSelector';\nexport { default as URLManager } from './URLManager';\n\nexport * from './Plugins';\nexport * from './StandardIcons';\nexport * from './Utils';\n"
  },
  {
    "path": "src/client/Graphs/GraphLineChart.js",
    "content": "import React from 'react';\nimport {\n    CartesianGrid, Legend, Line, LineChart, ResponsiveContainer,\n    Tooltip, XAxis, YAxis,\n} from 'recharts';\n\nimport PropTypes from '../prop-types';\n\nfunction GraphLineChart(props) {\n    const lineElements = props.lines.map((lineItem) => (\n        <Line\n            key={lineItem.name}\n            name={lineItem.name}\n            dataKey={lineItem.dataKey}\n            type=\"monotone\"\n            stroke={lineItem.color || '#00bc8c'}\n            connectNulls\n            isAnimationActive={false}\n        />\n    ));\n    return (\n        <ResponsiveContainer width=\"100%\" height={400}>\n            <LineChart\n                data={props.samples}\n                margin={{\n                    top: 20, right: 40, bottom: 50, left: 10,\n                }}\n            >\n                <CartesianGrid strokeDasharray=\"3 3\" />\n                <XAxis dataKey=\"label\" angle={-90} dx={-6} dy={40} />\n                <YAxis domain={['dataMin', 'dataMax']} scale=\"linear\" />\n                <Legend />\n                {props.tooltip ? <Tooltip content={props.tooltip} /> : null}\n                {lineElements}\n            </LineChart>\n        </ResponsiveContainer>\n    );\n}\n\nconst LineItem = PropTypes.shape({\n    name: PropTypes.string.isRequired,\n    dataKey: PropTypes.string.isRequired,\n});\n\nGraphLineChart.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    samples: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired,\n    lines: PropTypes.arrayOf(LineItem.isRequired).isRequired,\n    tooltip: PropTypes.func,\n};\n\nexport default GraphLineChart;\n"
  },
  {
    "path": "src/client/Graphs/GraphSection.css",
    "content": ".graph-tooltip {\n    background-color: var(--component-color);\n    padding: 4px;\n    border-radius: 4px;\n    white-space: pre-wrap;\n}\n"
  },
  {
    "path": "src/client/Graphs/GraphSection.js",
    "content": "import './GraphSection.css';\n\nimport deepEqual from 'deep-equal';\nimport React from 'react';\n\nimport { DataLoader } from '../Common';\nimport PropTypes from '../prop-types';\nimport GraphLineChart from './GraphLineChart';\nimport { getGraphData } from './GraphSectionData';\nimport GraphSectionOptions, { Granularity } from './GraphSectionOptions';\nimport { NormalTooltip } from './GraphTooltip';\n\nclass GraphSection extends React.Component {\n    static getTypeaheadOptions() {\n        return GraphSectionOptions.get();\n    }\n\n    static getDerivedStateFromProps(props, state) {\n        const result = GraphSectionOptions.extractData(props.search);\n        result.where.date = props.dateRange || undefined;\n        const newGranularity = result.extra.granularity || Granularity.WEEK;\n        if (!deepEqual(state.where, result.where)) {\n            state.reload = true;\n        }\n        state.where = result.where;\n        state.hasAnyFilters = props.search.length > 0;\n        if (state.granularity !== newGranularity && state.logEvents) {\n            state.graphData = getGraphData(\n                state.where.logStructure,\n                state.logEvents,\n                props.dateRange,\n                newGranularity,\n            );\n        }\n        state.granularity = newGranularity;\n        return state;\n    }\n\n    constructor(props) {\n        super(props);\n        this.state = { graphData: null };\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => {\n                const { hasAnyFilters, where } = this.state;\n                if (!hasAnyFilters) {\n                    return null;\n                }\n                return {\n                    name: 'log-event-list',\n                    args: {\n                        where: {\n                            ...where,\n                            date: this.props.dateRange || undefined,\n                        },\n                    },\n                };\n            },\n            onData: (logEvents) => {\n                const { dateRange } = this.props;\n                const { where, granularity } = this.state;\n                const graphData = getGraphData(\n                    where.logStructure,\n                    logEvents,\n                    dateRange,\n                    granularity,\n                );\n                this.setState({ logEvents, graphData });\n            },\n        });\n    }\n\n    componentDidUpdate(prevProps) {\n        if (this.state.reload) {\n            // eslint-disable-next-line react/no-did-update-set-state\n            this.setState({ reload: false });\n            this.dataLoader.reload();\n        }\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    render() {\n        const { hasAnyFilters, graphData } = this.state;\n        if (!hasAnyFilters) {\n            return 'Please add some filters!';\n        } if (!graphData) {\n            return 'Loading ...';\n        } if (graphData.isEmpty) {\n            return 'No data!';\n        }\n        return graphData.lines.map((line) => {\n            const lines = [{\n                name: line.name,\n                dataKey: line.valueKey,\n            }];\n            return (\n                <GraphLineChart\n                    key={line.name}\n                    samples={graphData.samples}\n                    lines={lines}\n                    tooltip={NormalTooltip}\n                />\n            );\n        });\n    }\n}\n\nGraphSection.propTypes = {\n    dateRange: PropTypes.Custom.DateRange,\n    search: PropTypes.arrayOf(PropTypes.Custom.Item.isRequired).isRequired,\n};\n\nexport default GraphSection;\n"
  },
  {
    "path": "src/client/Graphs/GraphSectionData.js",
    "content": "import { addDays, compareAsc } from 'date-fns';\n\nimport { LogKey } from '../../common/data_types';\nimport DateUtils from '../../common/DateUtils';\nimport RichTextUtils from '../../common/RichTextUtils';\nimport { Granularity } from './GraphSectionOptions';\n\nfunction getLogKeyValues(keyIndex, valueParser, logEvents) {\n    return logEvents.map((logEvent) => {\n        const logKey = logEvent.logStructure.eventKeys[keyIndex];\n        if (logKey.value === null) {\n            return null;\n        }\n        return valueParser(logKey.value);\n    }).filter((value) => value !== null);\n}\n\nfunction getAverageValue(values) {\n    if (values.length === 0) {\n        return null;\n    } if (values.length === 1) {\n        return values[0];\n    }\n    // logEvents[0].logStructure.eventKeys[keyIndex].aggregationType?\n    return values.reduce((result, value) => (result + value), 0) / values.length;\n}\n\nfunction getLines(logStructure, logEvent) {\n    const lines = [];\n    lines.push({\n        valueKey: 'event_count',\n        valuesKey: 'event_count_values',\n        name: 'Event Count',\n        getValues: (logEvents) => [logEvents.length],\n    });\n    if (logStructure) {\n        logEvent.logStructure.eventKeys.forEach((logKey, index) => {\n            let valueParser;\n            if (logKey.type === LogKey.Type.INTEGER) {\n                valueParser = parseInt;\n            } else if (logKey.type === LogKey.Type.NUMBER) {\n                valueParser = parseFloat;\n            } else if (logKey.type === LogKey.Type.TIME) {\n                valueParser = (value) => parseInt(value.replace(':', ''), 10);\n            } else {\n                return;\n            }\n            lines.push({\n                valueKey: `key_${logKey.__id__}_value`,\n                valuesKey: `key_${logKey.__id__}_values`,\n                name: `Key: ${logKey.name}`,\n                getValues: getLogKeyValues.bind(null, index, valueParser),\n            });\n        });\n    }\n    // TODO: Add support for custom graphs?\n    return lines;\n}\n\nfunction getTimeSeries(logEvents, lines, dateRange, granularity) {\n    if (logEvents.length === 0) {\n        return [];\n    }\n    const dateLabelToLogEvents = {};\n    logEvents.forEach((item) => {\n        if (!item.date) {\n            return;\n        }\n        if (!(item.date in dateLabelToLogEvents)) {\n            dateLabelToLogEvents[item.date] = [];\n        }\n        dateLabelToLogEvents[item.date].push(item);\n    });\n    let startDate;\n    let endDate;\n    if (dateRange) {\n        startDate = DateUtils.getDate(dateRange.startDate);\n        endDate = DateUtils.getDate(dateRange.endDate);\n    } else {\n        const dateLabels = Object.keys(dateLabelToLogEvents).sort();\n        startDate = DateUtils.getDate(dateLabels[0]);\n        endDate = DateUtils.getDate(dateLabels[dateLabels.length - 1]);\n    }\n    const samples = [];\n    for (\n        let currentDate = startDate;\n        compareAsc(currentDate, endDate) <= 0;\n    ) {\n        const currentLogEvents = [];\n        let label = null;\n        // eslint-disable-next-line no-constant-condition\n        while (true) {\n            const nextLabel = Granularity[granularity].getLabel(currentDate);\n            if (label === null) {\n                label = nextLabel;\n            } else if (label === nextLabel) {\n                // nothing changes\n            } else {\n                break; // move to next group\n            }\n            const dateLabel = DateUtils.getLabel(currentDate);\n            const nextLogEvents = dateLabelToLogEvents[dateLabel] || [];\n            currentLogEvents.push(...nextLogEvents);\n            currentDate = addDays(currentDate, 1);\n        }\n        const sample = { label };\n        lines.forEach((line) => {\n            const values = line.getValues(currentLogEvents);\n            sample[line.valuesKey] = values;\n            sample[line.valueKey] = getAverageValue(values);\n        });\n        sample.logEventTitles = currentLogEvents.map(\n            (logEvent) => `${logEvent.date}: ${RichTextUtils.extractPlainText(logEvent.title)}`,\n        );\n        samples.push(sample);\n    }\n    return samples;\n}\n\n// eslint-disable-next-line import/prefer-default-export\nexport function getGraphData(logStructure, logEvents, dateRange, granularity) {\n    if (!logEvents || !logEvents.length) return { isEmpty: true };\n    try {\n        const lines = getLines(logStructure, logEvents[0]);\n        const samples = getTimeSeries(logEvents, lines, dateRange, granularity);\n        return { lines, samples };\n    } catch (error) {\n        // eslint-disable-next-line no-console\n        console.error(error);\n        throw error;\n    }\n}\n"
  },
  {
    "path": "src/client/Graphs/GraphSectionOptions.js",
    "content": "import {\n    getDay, getMonth, getYear, subDays,\n} from 'date-fns';\n\nimport { Enum } from '../../common/data_types';\nimport DateUtils from '../../common/DateUtils';\nimport { LogEventOptions } from '../LogEvent';\n\nconst Granularity = Enum([\n    {\n        label: 'Day',\n        value: 'day',\n        getLabel: (date) => DateUtils.getLabel(date),\n    },\n    {\n        label: 'Week',\n        value: 'week',\n        getLabel: (date) => {\n            const dayOfWeek = getDay(date);\n            const startDateOfWeek = subDays(date, dayOfWeek);\n            return DateUtils.getLabel(startDateOfWeek);\n        },\n    },\n    {\n        label: 'Month',\n        value: 'month',\n        getLabel: (date) => {\n            let month = (getMonth(date) + 1).toString();\n            month = (month.length === 1 ? '0' : '') + month;\n            return `${getYear(date)}-${month}`;\n        },\n    },\n]);\n\nconst GRANULARITY_TYPE = 'graph-granularity';\nconst GRANULARITY_PREFIX = 'Granularity: ';\nconst GRANULARITY_OPTIONS = Granularity.Options.map((option, index) => ({\n    __type__: GRANULARITY_TYPE,\n    __id__: -index - 1,\n    name: GRANULARITY_PREFIX + option.label,\n}));\n\nconst GRANULARITY_MOCK_OPTION = {\n    __type__: GRANULARITY_TYPE,\n    apply: (item, _where, extra) => {\n        extra.granularity = item.name.substr(GRANULARITY_PREFIX.length).toLowerCase();\n    },\n};\n\nclass GraphSectionOptions {\n    static get() {\n        return LogEventOptions.get(GRANULARITY_OPTIONS);\n    }\n\n    static extractData(items) {\n        const result = LogEventOptions.extractData(\n            items,\n            LogEventOptions.getTypeToActionMap([GRANULARITY_MOCK_OPTION]),\n        );\n        return result;\n    }\n}\n\nexport { Granularity };\nexport default GraphSectionOptions;\n"
  },
  {
    "path": "src/client/Graphs/GraphTooltip.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nfunction NormalTooltip({ active, label, payload }) {\n    if (active && payload && payload.length) {\n        const item = payload[0];\n        const output = [];\n        output.push(`Group: ${label}`);\n        output.push(`${item.name}: ${item.payload[item.dataKey]}`);\n        const { logEventTitles } = item.payload;\n        if (logEventTitles.length) {\n            output.push('', ...logEventTitles);\n        }\n        return (\n            <div className=\"graph-tooltip\">\n                {output.map((line) => `${line}\\n`).join('')}\n            </div>\n        );\n    }\n    return null;\n}\n\nNormalTooltip.propTypes = {\n    active: PropTypes.bool,\n    label: PropTypes.string,\n    // eslint-disable-next-line react/forbid-prop-types\n    payload: PropTypes.any,\n};\n\n// eslint-disable-next-line import/prefer-default-export\nexport { NormalTooltip };\n"
  },
  {
    "path": "src/client/Graphs/index.js",
    "content": "export { default as GraphSection } from './GraphSection';\nexport { default as GraphLineChart } from './GraphLineChart';\nexport { Granularity } from './GraphSectionOptions';\nexport { getGraphData } from './GraphSectionData';\n"
  },
  {
    "path": "src/client/LogEvent/LogEventAdder.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { isRealItem, LogEvent } from '../../common/data_types';\nimport {\n    Coordinator, KeyCodes, TextEditor, TypeaheadOptions,\n} from '../Common';\nimport LogEventEditor from './LogEventEditor';\n\nclass LogEventAdder extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            logEvent: LogEvent.createVirtual(this.props.where),\n        };\n    }\n\n    onEditLogEvent(logEvent) {\n        this.setState({ logEvent: LogEvent.createVirtual(this.props.where) });\n        Coordinator.invoke('modal-editor', {\n            dataType: 'log-event',\n            EditorComponent: LogEventEditor,\n            valueKey: 'logEvent',\n            value: logEvent,\n        });\n    }\n\n    onSaveLogEvent(logEvent) {\n        if (logEvent.title) {\n            window.api.send('log-event-upsert', logEvent)\n                .then((_newLogEvent) => {\n                    // The new LogEvent would have been added to list, so we can reset this.\n                    this.setState({ logEvent: LogEvent.createVirtual(this.props.where) });\n                });\n        } else {\n            this.onEditLogEvent(logEvent);\n        }\n    }\n\n    async onSelect(option) {\n        if (option.__type__ === 'log-structure') {\n            const logStructure = await window.api.send('log-structure-load', option);\n            const updatedLogEvent = LogEvent.createVirtual({\n                ...this.props.where,\n                logStructure,\n            });\n            LogEvent.trigger(updatedLogEvent);\n            if (logStructure.eventNeedsEdit) {\n                this.onEditLogEvent(updatedLogEvent);\n            } else {\n                this.onSaveLogEvent(updatedLogEvent);\n            }\n        }\n    }\n\n    render() {\n        const { logEvent } = this.state;\n        return (\n            <TextEditor\n                isSingleLine\n                focusOnLoad\n                unstyled\n                minWidth\n                placeholder=\"Add Event ...\"\n                value={logEvent.title}\n                options={new TypeaheadOptions({\n                    serverSideOptions: [{ name: 'log-structure' }, { name: 'log-topic' }],\n                    onSelect: (option) => this.onSelect(option),\n                })}\n                disabled={isRealItem(logEvent.logStructure)}\n                onChange={(value) => {\n                    const updatedLogEvent = { ...logEvent };\n                    updatedLogEvent.title = value;\n                    LogEvent.trigger(updatedLogEvent);\n                    this.setState({ logEvent: updatedLogEvent });\n                }}\n                onSpecialKeys={(event) => {\n                    if (event.keyCode === KeyCodes.ENTER) {\n                        this.onSaveLogEvent(logEvent);\n                    }\n                }}\n                {...this.props}\n            />\n        );\n    }\n}\n\nLogEventAdder.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    where: PropTypes.object,\n};\n\nLogEventAdder.defaultProps = {\n    where: {},\n};\n\nexport default LogEventAdder;\n"
  },
  {
    "path": "src/client/LogEvent/LogEventDetailsHeader.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    InputLine, TextEditor,\n} from '../Common';\n\nclass LogEventDetailsHeader extends React.Component {\n    renderTitle() {\n        const { logEvent } = this.props;\n        return (\n            <InputLine styled className=\"px-2\">\n                <TextEditor\n                    isSingleLine\n                    unstyled\n                    disabled\n                    value={logEvent.title}\n                />\n            </InputLine>\n        );\n    }\n\n    render() {\n        return this.renderTitle();\n    }\n}\n\nLogEventDetailsHeader.propTypes = {\n    logEvent: PropTypes.Custom.LogEvent.isRequired,\n};\n\nexport default LogEventDetailsHeader;\n"
  },
  {
    "path": "src/client/LogEvent/LogEventEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { LogEvent } from '../../common/data_types';\nimport {\n    DatePicker, Selector, TextEditor, TypeaheadOptions, TypeaheadSelector,\n} from '../Common';\nimport { LogValueListEditor } from '../LogKey';\n\nconst { LogLevel } = LogEvent;\n\nclass LogEventEditor extends React.Component {\n    constructor(props) {\n        super(props);\n        this.titleRef = React.createRef();\n        this.detailsRef = React.createRef();\n        this.valueListRef = React.createRef();\n    }\n\n    componentDidMount() {\n        const { logEvent } = this.props;\n        if (logEvent.logStructure) {\n            if (logEvent.logStructure.eventKeys.length) {\n                this.valueListRef.current.focus();\n            } else {\n                this.detailsRef.current.focus();\n            }\n        } else {\n            this.titleRef.current.focus();\n        }\n    }\n\n    updateLogEvent(methodOrName, maybeValue) {\n        const updatedLogEvent = { ...this.props.logEvent };\n        if (typeof methodOrName === 'function') {\n            methodOrName(updatedLogEvent);\n        } else {\n            updatedLogEvent[methodOrName] = maybeValue;\n        }\n        LogEvent.trigger(updatedLogEvent);\n        this.props.onChange(updatedLogEvent);\n    }\n\n    renderDate() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    {this.props.logEvent.isComplete ? 'Date' : 'Deadline Date'}\n                </InputGroup.Text>\n                <DatePicker\n                    date={this.props.logEvent.date}\n                    disabled={this.props.disabled}\n                    onChange={(date) => this.updateLogEvent('date', date)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderIsComplete() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Complete?\n                </InputGroup.Text>\n                <Selector.Binary\n                    value={this.props.logEvent.isComplete}\n                    disabled={this.props.disabled}\n                    onChange={(isComplete) => this.updateLogEvent('isComplete', isComplete)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderTitle() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Title\n                </InputGroup.Text>\n                <TextEditor\n                    isSingleLine\n                    value={this.props.logEvent.title}\n                    options={new TypeaheadOptions({\n                        serverSideOptions: [{ name: 'log-structure' }, { name: 'log-topic' }],\n                        onSelect: async (option) => {\n                            if (option.__type__ === 'log-structure') {\n                                const logStructure = await window.api.send('log-structure-load', option);\n                                this.updateLogEvent('logStructure', logStructure);\n                            }\n                        },\n                    })}\n                    disabled={this.props.disabled || !!this.props.logEvent.logStructure}\n                    onChange={(title) => this.updateLogEvent('title', title)}\n                    onSpecialKeys={this.props.onSpecialKeys}\n                    ref={this.titleRef}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderDetails() {\n        const { logEvent } = this.props;\n        const eventAllowDetails = logEvent.logStructure\n            ? logEvent.logStructure.eventAllowDetails\n            : true;\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Details\n                </InputGroup.Text>\n                <TextEditor\n                    value={logEvent.details}\n                    options={TypeaheadOptions.getFromTypes(['log-topic'])}\n                    disabled={this.props.disabled || !eventAllowDetails}\n                    onChange={(details) => this.updateLogEvent('details', details)}\n                    ref={this.detailsRef}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderLogLevel() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Log Level\n                </InputGroup.Text>\n                <Selector\n                    options={LogLevel.Options}\n                    value={LogLevel.getValue(this.props.logEvent.logLevel)}\n                    disabled={this.props.disabled}\n                    onChange={(value) => this.updateLogEvent('logLevel', LogLevel.getIndex(value))}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderStructureSelector() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Structure\n                </InputGroup.Text>\n                <TypeaheadSelector\n                    id=\"log-event-editor-structure\"\n                    options={new TypeaheadOptions({\n                        serverSideOptions: [{ name: 'log-structure' }],\n                        onSelect: (option) => window.api.send('log-structure-load', option),\n                    })}\n                    value={this.props.logEvent.logStructure}\n                    disabled={this.props.disabled}\n                    onChange={(logStructure) => this.updateLogEvent((updatedLogEvent) => {\n                        updatedLogEvent.logStructure = logStructure;\n                        if (logStructure) {\n                            LogEvent.addDefaultStructureValues(updatedLogEvent);\n                        } else {\n                            updatedLogEvent.title = null;\n                        }\n                    })}\n                    allowDelete\n                />\n            </InputGroup>\n        );\n    }\n\n    renderStructureValues() {\n        const { logEvent } = this.props;\n        if (!logEvent.logStructure || logEvent.logStructure.eventKeys.length === 0) {\n            return null;\n        }\n        return (\n            <LogValueListEditor\n                source={logEvent.logStructure}\n                logKeys={logEvent.logStructure.eventKeys}\n                disabled={this.props.disabled}\n                onChange={(updatedLogKeys) => this.updateLogEvent((updatedLogEvent) => {\n                    updatedLogEvent.logStructure.eventKeys = updatedLogKeys;\n                })}\n                ref={this.valueListRef}\n            />\n        );\n    }\n\n    render() {\n        return (\n            <div>\n                <div className=\"my-3\">\n                    {this.renderDate()}\n                    {this.renderIsComplete()}\n                </div>\n                <div className=\"my-3\">\n                    {this.renderTitle()}\n                    {this.renderDetails()}\n                </div>\n                {this.renderLogLevel()}\n                <div className=\"my-3\">\n                    {this.renderStructureSelector()}\n                    {this.renderStructureValues()}\n                </div>\n            </div>\n        );\n    }\n}\n\nLogEventEditor.propTypes = {\n    logEvent: PropTypes.Custom.LogEvent.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n    onSpecialKeys: PropTypes.func,\n};\n\nexport default LogEventEditor;\n"
  },
  {
    "path": "src/client/LogEvent/LogEventList.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { BulletList, DetailsIcon, TextEditor } from '../Common';\nimport LogEventAdder from './LogEventAdder';\nimport LogEventEditor from './LogEventEditor';\n\nfunction LogEventViewer(props) {\n    const { logEvent } = props;\n    let datePrefix;\n    if (props.displayDate) {\n        datePrefix = (\n            <span className=\"float-left monospace\">\n                {`${logEvent.date}: `}\n            </span>\n        );\n    }\n    const title = (\n        <span className=\"ml-1\">\n            <TextEditor.SimpleViewer\n                unstyled\n                disabled\n                value={logEvent.title}\n            />\n        </span>\n    );\n    let detailsSuffix;\n    if (logEvent.details) {\n        detailsSuffix = (\n            <DetailsIcon\n                onClick={props.toggleExpansion}\n                isShown\n            />\n        );\n    }\n    let logLevelSuffix;\n    if (props.displayLogLevel) {\n        logLevelSuffix = (\n            <span className=\"float-right ml-1\">\n                {`L${logEvent.logLevel}`}\n            </span>\n        );\n    }\n    return (\n        <div>\n            {datePrefix}\n            {logLevelSuffix}\n            {title}\n            {detailsSuffix}\n        </div>\n    );\n}\n\nLogEventViewer.propTypes = {\n    logEvent: PropTypes.Custom.LogEvent.isRequired,\n    displayDate: PropTypes.bool,\n    displayLogLevel: PropTypes.bool,\n    toggleExpansion: PropTypes.func.isRequired,\n};\n\nLogEventViewer.Expanded = function (props) {\n    const { logEvent } = props;\n    if (!logEvent.details) {\n        return null;\n    }\n    return (\n        <TextEditor\n            unstyled\n            disabled\n            value={logEvent.details}\n        />\n    );\n};\n\nLogEventViewer.Expanded.propTypes = {\n    logEvent: PropTypes.Custom.LogEvent.isRequired,\n};\n\nfunction LogEventList(props) {\n    const { showAdder, ...moreProps } = props;\n    return (\n        <BulletList\n            {...moreProps}\n            dataType=\"log-event\"\n            valueKey=\"logEvent\"\n            allowSubscription\n            ViewerComponent={LogEventViewer}\n            EditorComponent={LogEventEditor}\n            AdderComponent={showAdder ? LogEventAdder : null}\n        />\n    );\n}\n\nLogEventList.propTypes = {\n    name: PropTypes.string.isRequired,\n    showAdder: PropTypes.bool,\n};\n\nLogEventList.Single = function (props) {\n    const { logEvent, ...moreProps } = props;\n    return (\n        <BulletList.Item\n            {...moreProps}\n            dataType=\"log-event\"\n            value={logEvent}\n            valueKey=\"logEvent\"\n            ViewerComponent={LogEventViewer}\n            EditorComponent={LogEventEditor}\n        />\n    );\n};\n\nLogEventList.Single.propTypes = {\n    logEvent: PropTypes.Custom.LogEvent.isRequired,\n};\n\nexport default LogEventList;\n"
  },
  {
    "path": "src/client/LogEvent/LogEventOptions.js",
    "content": "import assert from 'assert';\n\nimport { getVirtualID } from '../../common/data_types';\nimport { TypeaheadOptions } from '../Common';\n\nconst NO_STRUCTURE_ITEM = {\n    __type__: 'log-structure',\n    __id__: 0,\n    name: 'No Structure',\n};\n\nconst EVENT_TITLE_ITEM_TYPE = 'log-event-title';\nconst EVENT_TITLE_ITEM_PREFIX = 'Title: ';\n\nclass LogEventOptions {\n    static get(prefixOptions) {\n        prefixOptions = [...prefixOptions, NO_STRUCTURE_ITEM];\n        return new TypeaheadOptions({\n            serverSideOptions: [\n                { name: 'log-topic' },\n                { name: 'log-structure' },\n            ],\n            prefixOptions,\n            computedOptionTypes: [EVENT_TITLE_ITEM_TYPE],\n            getComputedOptions: async (query) => {\n                const options = [];\n                if (query) {\n                    options.push({\n                        __type__: EVENT_TITLE_ITEM_TYPE,\n                        __id__: getVirtualID(),\n                        name: EVENT_TITLE_ITEM_PREFIX + query,\n                    });\n                }\n                return options;\n            },\n            onSelect: (option) => {\n                if (option && option.getItem) {\n                    return option.getItem(option);\n                }\n                return undefined;\n            },\n        });\n    }\n\n    static getTypeToActionMap(extraOptions) {\n        const result = {\n            'log-structure': (item, where, extra) => {\n                // This also handles NO_STRUCTURE_ITEM.\n                assert(!Object.prototype.hasOwnProperty.call(where, 'logStructure'));\n                where.logStructure = item.__id__ ? item : null;\n                extra.searchView = true;\n            },\n            'log-topic': (item, where, extra) => {\n                if (!where.logTopics) {\n                    where.logTopics = [];\n                }\n                where.logTopics.push(item);\n                extra.searchView = true;\n            },\n            [EVENT_TITLE_ITEM_TYPE]: (item, where, extra) => {\n                where.title = item.name.substring(EVENT_TITLE_ITEM_PREFIX.length);\n                extra.searchView = true;\n            },\n        };\n        if (extraOptions) {\n            extraOptions.forEach((item) => {\n                assert(typeof item.apply === 'function', `Missing apply method on ${item}`);\n                result[item.__type__] = item.apply;\n            });\n        }\n        return result;\n    }\n\n    static extractData(items, typeToActionMap, defaultWhere) {\n        const where = { ...defaultWhere };\n        const extra = {};\n        items.forEach((item) => {\n            const action = typeToActionMap[item.__type__];\n            if (action) {\n                action(item, where, extra);\n            } else {\n                assert(false, `Unable to process ${JSON.stringify(item)}`);\n            }\n        });\n        return { where, extra };\n    }\n}\n\nexport default LogEventOptions;\n"
  },
  {
    "path": "src/client/LogEvent/LogEventSearch.js",
    "content": "import assert from 'assert';\nimport { addDays, eachDayOfInterval, getDay } from 'date-fns';\nimport React from 'react';\n\nimport { getVirtualID, LogEvent } from '../../common/data_types';\nimport DateUtils from '../../common/DateUtils';\nimport { Coordinator, DateContext, SettingsContext } from '../Common';\nimport PropTypes from '../prop-types';\nimport LogEventEditor from './LogEventEditor';\nimport LogEventList from './LogEventList';\nimport LogEventOptions from './LogEventOptions';\n\n// Extra Filters for Events\n\nconst INCOMPLETE_ITEM = {\n    __type__: 'incomplete',\n    __id__: getVirtualID(),\n    name: 'Incomplete Events',\n    apply: (_item, where, _extra) => {\n        where.isComplete = false;\n    },\n};\n\nconst LOG_LEVEL_MINOR_ITEM = {\n    __type__: 'log-event-level',\n    __id__: getVirtualID(),\n    name: 'Log Level: Minor+',\n};\nconst LOG_LEVEL_MAJOR_ITEM = {\n    __type__: 'log-event-level',\n    __id__: getVirtualID(),\n    name: 'Log Level: Major+',\n};\nconst LOG_LEVEL_MOCK_ITEM = {\n    __type__: 'log-event-level',\n    apply: (item, where, extra) => {\n        if (item.__id__ === LOG_LEVEL_MINOR_ITEM.__id__) {\n            delete where.logLevel; // [1, 2, 3]\n            extra.allowReordering = true;\n        } else if (item.__id__ === LOG_LEVEL_MAJOR_ITEM.__id__) {\n            where.logLevel = [3];\n            extra.searchView = true;\n        }\n    },\n};\n\nconst DEFAULT_WHERE = {\n    isComplete: true,\n    logLevel: [2, 3],\n};\n\nfunction getDayOfTheWeek(label) {\n    return DateUtils.DaysOfTheWeek[getDay(DateUtils.getDate(label))];\n}\n\nclass LogEventSearch extends React.Component {\n    static getTypeaheadOptions() {\n        return LogEventOptions.get([\n            INCOMPLETE_ITEM,\n            LOG_LEVEL_MINOR_ITEM,\n            LOG_LEVEL_MAJOR_ITEM,\n        ]);\n    }\n\n    static getDerivedStateFromProps(props, _state) {\n        return LogEventOptions.extractData(\n            props.search,\n            LogEventOptions.getTypeToActionMap([\n                INCOMPLETE_ITEM,\n                LOG_LEVEL_MOCK_ITEM,\n            ]),\n            DEFAULT_WHERE,\n        );\n    }\n\n    constructor(props) {\n        super(props);\n        this.state = {};\n    }\n\n    componentDidMount() {\n        this.deregisterCallbacks = [\n            Coordinator.subscribe('log-event-created', (logEvent) => {\n                if (logEvent.logLevel === 1 && !this.props.search.length) {\n                    Coordinator.invoke('url-update', { search: [LOG_LEVEL_MINOR_ITEM] });\n                }\n            }),\n        ];\n    }\n\n    componentWillUnmount() {\n        this.deregisterCallbacks.forEach((deregisterCallback) => deregisterCallback());\n    }\n\n    // Extra Actions for Events\n\n    getPlanForTodayAction() {\n        const { todayLabel } = this.context;\n        return {\n            __id__: 'plan_for_today',\n            name: 'Plan for Today',\n            perform: (logEvent) => {\n                window.api.send('log-event-upsert', {\n                    ...logEvent,\n                    date: todayLabel,\n                    isComplete: false,\n                });\n            },\n        };\n    }\n\n    getCompleteAction() {\n        const { todayLabel } = this.context;\n        return {\n            __id__: 'complete',\n            name: 'Complete',\n            perform: (logEvent) => {\n                window.api.send('log-event-upsert', {\n                    ...logEvent,\n                    date: todayLabel,\n                    isComplete: true,\n                });\n            },\n        };\n    }\n\n    getDuplicateAction() {\n        const { todayLabel } = this.context;\n        return {\n            __id__: 'duplicate_for_today',\n            name: 'Duplicate for Today',\n            perform: (logEvent) => {\n                Coordinator.invoke('modal-editor', {\n                    dataType: 'log-event',\n                    EditorComponent: LogEventEditor,\n                    valueKey: 'logEvent',\n                    value: LogEvent.createVirtual({\n                        ...logEvent,\n                        date: logEvent.date ? todayLabel : null,\n                    }),\n                });\n            },\n        };\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    renderDefaultView(where, moreProps, settings) {\n        const { todayDate, todayLabel } = this.context;\n        const todoMoreProps = {\n            ...moreProps,\n            prefixActions: [this.getCompleteAction(), ...moreProps.prefixActions],\n        };\n        const overdueAndUpcomingMoreProps = {\n            ...todoMoreProps,\n            viewerComponentProps: {\n                ...todoMoreProps.viewerComponentProps,\n                displayDate: true,\n            },\n            prefixActions: [this.getPlanForTodayAction(), ...todoMoreProps.prefixActions],\n        };\n        const results = [\n            <LogEventList\n                key=\"done\"\n                name=\"Done: Today\"\n                where={{ date: todayLabel, ...where, isComplete: true }}\n                showAdder\n                {...moreProps}\n            />,\n            <LogEventList\n                key=\"todo\"\n                className=\"mt-4\"\n                name=\"Todo: Today\"\n                where={{\n                    date: todayLabel, ...where, isComplete: false,\n                }}\n                showAdder\n                {...todoMoreProps}\n            />,\n        ];\n        if (settings.display_overdue_and_upcoming_events) {\n            results.push(\n                <LogEventList\n                    key=\"tomorrow\"\n                    className=\"mt-4\"\n                    name=\"Todo: Tomorrow\"\n                    where={{\n                        date: DateUtils.getLabel(addDays(todayDate, 1)),\n                        ...where,\n                        isComplete: false,\n                    }}\n                    showAdder\n                    {...overdueAndUpcomingMoreProps}\n                />,\n                <LogEventList\n                    key=\"upcoming\"\n                    className=\"mt-4\"\n                    name=\"Todo: Next 7 days\"\n                    where={{\n                        date: {\n                            startDate: DateUtils.getLabel(addDays(todayDate, 2)),\n                            endDate: DateUtils.getLabel(addDays(todayDate, 7)),\n                        },\n                        ...where,\n                        isComplete: false,\n                    }}\n                    {...overdueAndUpcomingMoreProps}\n                />,\n                <LogEventList\n                    key=\"overdue\"\n                    className=\"mt-4\"\n                    name=\"Todo: Overdue\"\n                    where={{\n                        date: `lt(${todayLabel})`, ...where, isComplete: false,\n                    }}\n                    {...overdueAndUpcomingMoreProps}\n                />,\n            );\n        }\n        return results;\n    }\n\n    renderMultipleDaysView(where, moreProps) {\n        return eachDayOfInterval({\n            start: DateUtils.getDate(this.props.dateRange.startDate),\n            end: DateUtils.getDate(this.props.dateRange.endDate),\n        }).map((date) => {\n            const dateLabel = DateUtils.getLabel(date);\n            return (\n                <LogEventList\n                    key={dateLabel}\n                    name={`${dateLabel} (${getDayOfTheWeek(dateLabel)})`}\n                    where={{ date: dateLabel, ...where }}\n                    showAdder\n                    {...moreProps}\n                />\n            );\n        });\n    }\n\n    renderSearchView(where, moreProps) {\n        assert(where.isComplete);\n        const displayDateMoreProps = {\n            ...moreProps,\n            viewerComponentProps: {\n                ...moreProps.viewerComponentProps,\n                displayDate: true,\n            },\n        };\n        if (this.props.dateRange) {\n            where = { ...where, date: this.props.dateRange };\n        }\n        return (\n            <>\n                <LogEventList\n                    name=\"Complete\"\n                    where={{ ...where, isComplete: true }}\n                    {...displayDateMoreProps}\n                />\n                <LogEventList\n                    name=\"Incomplete (with deadlines)\"\n                    where={{ ...where, isComplete: false, date: 'ne(null)' }}\n                    {...displayDateMoreProps}\n                />\n                <LogEventList\n                    name=\"Incomplete (without deadlines)\"\n                    where={{ ...where, isComplete: false, date: null }}\n                    {...moreProps}\n                />\n            </>\n        );\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    renderIncompleteView(where, moreProps) {\n        const displayDateMoreProps = {\n            ...moreProps,\n            viewerComponentProps: {\n                ...moreProps.viewerComponentProps,\n                displayDate: true,\n            },\n        };\n        return (\n            <>\n                <LogEventList\n                    name=\"With Deadlines\"\n                    where={{ ...where, date: 'ne(null)' }}\n                    {...displayDateMoreProps}\n                />\n                <LogEventList\n                    name=\"Without Deadlines\"\n                    where={{ ...where, date: null }}\n                    {...moreProps}\n                />\n            </>\n        );\n    }\n\n    render() {\n        const { where, extra } = this.state;\n\n        const moreProps = { viewerComponentProps: {} };\n        moreProps.prefixActions = [];\n        moreProps.prefixActions.push(this.getDuplicateAction());\n        if (extra.allowReordering) {\n            moreProps.allowReordering = true;\n            moreProps.viewerComponentProps.displayLogLevel = true;\n        }\n\n        if (where.isComplete === false) {\n            return this.renderIncompleteView(where, moreProps);\n        } if (extra.searchView) {\n            return this.renderSearchView(where, moreProps);\n        } if (this.props.dateRange) {\n            return this.renderMultipleDaysView(where, moreProps);\n        }\n        return (\n            <SettingsContext.Consumer>\n                {(settings) => this.renderDefaultView(where, moreProps, settings)}\n            </SettingsContext.Consumer>\n        );\n    }\n}\n\nLogEventSearch.propTypes = {\n    dateRange: PropTypes.Custom.DateRange,\n    search: PropTypes.arrayOf(PropTypes.Custom.Item.isRequired).isRequired,\n};\n\nLogEventSearch.contextType = DateContext;\n\nexport default LogEventSearch;\n"
  },
  {
    "path": "src/client/LogEvent/index.js",
    "content": "export { default as LogEventEditor } from './LogEventEditor';\nexport { default as LogEventList } from './LogEventList';\nexport { default as LogEventSearch } from './LogEventSearch';\nexport { default as LogEventDetailsHeader } from './LogEventDetailsHeader';\nexport { default as LogEventOptions } from './LogEventOptions';\n"
  },
  {
    "path": "src/client/LogKey/LogKeyEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { LogKey } from '../../common/data_types';\nimport {\n    Selector, TextEditor, TextInput, TypeaheadOptions, TypeaheadSelector,\n} from '../Common';\nimport LogStructureValueEditor from './LogValueEditor';\n\nclass LogKeyEditor extends React.Component {\n    static getDerivedStateFromProps(props) {\n        return {\n            logKey: props.logKeys[props.index],\n        };\n    }\n\n    constructor(props) {\n        super(props);\n        this.state = {};\n    }\n\n    update(methodOrName, maybeValue) {\n        const logKey = { ...this.state.logKey };\n        if (typeof methodOrName === 'function') {\n            methodOrName(logKey);\n        } else {\n            logKey[methodOrName] = maybeValue;\n        }\n        this.props.onChange(logKey);\n    }\n\n    updateType(newType) {\n        const logKey = { ...this.state.logKey };\n        logKey.type = newType;\n        logKey.value = LogKey.Type[newType].default;\n        this.props.onChange(logKey);\n    }\n\n    renderTypeSelector() {\n        return (\n            <Selector\n                value={this.state.logKey.type}\n                options={LogKey.Type.Options}\n                disabled={this.props.disabled}\n                onChange={(type) => this.updateType(type)}\n                style={{ borderRight: '2px solid transparent' }}\n            />\n        );\n    }\n\n    renderNameInput() {\n        return (\n            <TextInput\n                placeholder=\"Key Name\"\n                value={this.state.logKey.name}\n                disabled={this.props.disabled}\n                onChange={(name) => this.update('name', name)}\n            />\n        );\n    }\n\n    renderParentLogTopic() {\n        return (\n            <TypeaheadSelector\n                id=\"log-structure-key-editor-parent-topic\"\n                options={TypeaheadOptions.getFromTypes(['log-topic'])}\n                value={this.state.logKey.parentLogTopic}\n                disabled={this.props.disabled}\n                onChange={(parentLogTopic) => this.update('parentLogTopic', parentLogTopic)}\n                placeholder=\"Parent Topic\"\n            />\n        );\n    }\n\n    renderOptionalSelector() {\n        return (\n            <Selector.Binary\n                value={this.state.logKey.isOptional}\n                disabled={this.props.disabled}\n                onChange={(isOptional) => this.update('isOptional', isOptional)}\n                yesLabel=\"Optional\"\n                noLabel=\"Required\"\n                style={{ borderLeft: '1px solid transparent' }}\n            />\n        );\n    }\n\n    renderValue() {\n        if (this.state.logKey.isOptional) {\n            return null;\n        }\n        return (\n            <LogStructureValueEditor\n                logKey={this.state.logKey}\n                disabled={this.props.disabled}\n                onChange={this.props.onChange}\n                onSearch={(query) => Promise.resolve([])}\n            />\n        );\n    }\n\n    renderKeyTemplate() {\n        return (\n            <TextEditor\n                isSingleLine\n                value={this.state.logKey.template}\n                options={new TypeaheadOptions({\n                    prefixOptions: this.props.logKeys.slice(0, this.props.index),\n                    serverSideOptions: [],\n                })}\n                disabled={this.props.disabled}\n                onChange={(template) => this.update('template', template)}\n            />\n        );\n    }\n\n    renderEnumValuesSelector() {\n        const { logKey } = this.state;\n        return (\n            <TypeaheadSelector\n                id=\"log-structure-key-editor-enum-values\"\n                multiple\n                options={TypeaheadSelector.getStringListTypeaheadOptions(\n                    (query) => this.props.onSearch(query, this.props.index),\n                )}\n                value={TypeaheadSelector.getStringListItems(logKey.enumValues)}\n                disabled={this.props.disabled}\n                onChange={(items) => this.update('enumValues', items.map((item) => item.name))}\n                placeholder=\"Enum Values\"\n            />\n        );\n    }\n\n    renderEnumValuesSection() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text style={{ width: 100 }}>\n                    Enum Values\n                </InputGroup.Text>\n                {this.renderEnumValuesSelector()}\n            </InputGroup>\n        );\n    }\n\n    render() {\n        // eslint-disable-next-line react/prop-types\n        const children = this.props.children || [];\n        return (\n            <div className=\"log-structure-key my-2\">\n                <InputGroup className=\"my-1\">\n                    {children.shift()}\n                    <InputGroup.Text style={{ width: 108 }}>\n                        Key\n                    </InputGroup.Text>\n                    {this.renderTypeSelector()}\n                    {this.renderNameInput()}\n                    {this.state.logKey.type === LogKey.Type.LOG_TOPIC\n                        ? this.renderParentLogTopic() : null}\n                    {this.renderOptionalSelector()}\n                    {this.renderValue()}\n                    {children.pop()}\n                </InputGroup>\n                <InputGroup className=\"my-1\">\n                    <InputGroup.Text style={{ width: 128 }}>\n                        Key Template\n                    </InputGroup.Text>\n                    {this.renderKeyTemplate()}\n                </InputGroup>\n                {this.state.logKey.type === LogKey.Type.ENUM\n                    ? this.renderEnumValuesSection() : null}\n            </div>\n        );\n    }\n}\n\nLogKeyEditor.propTypes = {\n    logKeys: PropTypes.arrayOf(PropTypes.Custom.LogKey.isRequired).isRequired,\n    index: PropTypes.number.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n    onSearch: PropTypes.func.isRequired,\n};\n\nexport default LogKeyEditor;\n"
  },
  {
    "path": "src/client/LogKey/LogKeyListEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { MdAddCircleOutline } from 'react-icons/md';\n\nimport { LogKey } from '../../common/data_types';\nimport { SortableList, TextEditor, TypeaheadOptions } from '../Common';\nimport LogKeyEditor from './LogKeyEditor';\n\nclass LogKeyListEditor extends React.Component {\n    renderTitleTemplateEditor() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text style={{ height: 'inherit', width: 127 }}>\n                    {this.props.templateLabel}\n                </InputGroup.Text>\n                <TextEditor\n                    isSingleLine\n                    value={this.props.templateValue}\n                    options={this.props.templateOptions}\n                    disabled={this.props.disabled}\n                    onChange={(newTemplate) => this.props.onTemplateChange(newTemplate)}\n                />\n                <Button\n                    className=\"log-structure-add-key\"\n                    disabled={this.props.disabled}\n                    onClick={() => this.props.onLogKeysChange([\n                        ...this.props.logKeys,\n                        LogKey.createVirtual(),\n                    ])}\n                    style={{ height: 'inherit' }}\n                >\n                    <MdAddCircleOutline />\n                </Button>\n            </InputGroup>\n        );\n    }\n\n    renderSortableList() {\n        return (\n            <SortableList\n                items={this.props.logKeys}\n                disabled={this.props.disabled}\n                onChange={(updatedLogKeys) => this.props.onLogKeysChange(updatedLogKeys)}\n                onSearch={this.props.onValueSearch}\n                type={LogKeyEditor}\n                itemsKey=\"logKeys\"\n            />\n        );\n    }\n\n    render() {\n        return (\n            <>\n                {this.renderTitleTemplateEditor()}\n                {this.renderSortableList()}\n            </>\n        );\n    }\n}\n\nLogKeyListEditor.propTypes = {\n    templateLabel: PropTypes.string.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    templateValue: PropTypes.any,\n    templateOptions: PropTypes.instanceOf(TypeaheadOptions),\n    onTemplateChange: PropTypes.func.isRequired,\n    logKeys: PropTypes.arrayOf(PropTypes.Custom.LogKey.isRequired).isRequired,\n    onLogKeysChange: PropTypes.func.isRequired,\n    onValueSearch: PropTypes.func.isRequired,\n    disabled: PropTypes.bool.isRequired,\n};\n\nexport default LogKeyListEditor;\n"
  },
  {
    "path": "src/client/LogKey/LogValueEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { getPartialItem, LogKey } from '../../common/data_types';\nimport {\n    Selector, TextEditor, TypeaheadInput, TypeaheadOptions, TypeaheadSelector,\n} from '../Common';\nimport { LogTopicOptions } from '../LogTopic';\n\nclass LogValueEditor extends React.Component {\n    constructor(props) {\n        super(props);\n        this.ref = React.createRef();\n    }\n\n    focus() {\n        this.ref.current.focus();\n    }\n\n    update(value) {\n        const logKey = { ...this.props.logKey };\n        if (logKey.type === LogKey.Type.LOG_TOPIC && value) {\n            value = getPartialItem(value);\n        }\n        logKey.value = value;\n        this.props.onChange(logKey);\n    }\n\n    render() {\n        const { logKey } = this.props;\n        const disabled = this.props.disabled || !!logKey.template;\n        const uniqueId = `log-structure-value-editor-${logKey.__id__}`;\n        let { value } = logKey;\n        if (typeof value === 'undefined') {\n            value = LogKey.Type[logKey.type].default;\n        }\n        if (logKey.type === LogKey.Type.LINK && disabled) {\n            return (\n                <div className=\"pl-1\">\n                    <a href={value} target=\"new\" tabIndex={-1}>{value}</a>\n                </div>\n            );\n        } if (logKey.type === LogKey.Type.STRING_LIST) {\n            return (\n                <TypeaheadSelector\n                    id={uniqueId}\n                    options={TypeaheadSelector.getStringListTypeaheadOptions(this.props.onSearch)}\n                    value={TypeaheadSelector.getStringListItems(value)}\n                    disabled={disabled}\n                    onChange={(items) => this.update(items.map((item) => item.name))}\n                    multiple\n                    ref={this.ref}\n                />\n            );\n        } if (logKey.type === LogKey.Type.YES_OR_NO) {\n            return (\n                <Selector.Binary\n                    value={value === 'yes'}\n                    disabled={disabled}\n                    onChange={(newValue) => this.update(newValue ? 'yes' : 'no')}\n                    ref={this.ref}\n                />\n            );\n        } if (logKey.type === LogKey.Type.ENUM) {\n            return (\n                <Selector\n                    options={Selector.getStringListOptions(logKey.enumValues)}\n                    value={value}\n                    disabled={disabled}\n                    onChange={(newValue) => this.update(newValue)}\n                    ref={this.ref}\n                />\n            );\n        } if (logKey.type === LogKey.Type.LOG_TOPIC) {\n            const parentLogTopicId = logKey.parentLogTopic\n                ? logKey.parentLogTopic.__id__\n                : undefined;\n            return (\n                <TypeaheadSelector\n                    id={uniqueId}\n                    options={LogTopicOptions.get({\n                        allowCreation: true,\n                        parentLogTopic: logKey.parentLogTopic,\n                    })}\n                    value={value}\n                    disabled={disabled}\n                    onChange={(newValue) => this.update(newValue)}\n                    where={{ parent_topic_id: parentLogTopicId }}\n                    ref={this.ref}\n                />\n            );\n        } if (logKey.type === LogKey.Type.RICH_TEXT_LINE) {\n            return (\n                <TextEditor\n                    isSingleLine\n                    options={TypeaheadOptions.getFromTypes(['log-topic'])}\n                    value={value}\n                    disabled={disabled}\n                    onChange={(newValue) => this.update(newValue)}\n                    ref={this.ref}\n                />\n            );\n        }\n        return (\n            <TypeaheadInput\n                id={logKey.name}\n                value={value || ''}\n                disabled={disabled}\n                onChange={(newValue) => this.update(newValue)}\n                onSearch={(query) => this.props.onSearch(query)}\n                ref={this.ref}\n            />\n        );\n    }\n}\n\nLogValueEditor.propTypes = {\n    logKey: PropTypes.Custom.LogKey.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n    onSearch: PropTypes.func.isRequired,\n};\n\nexport default LogValueEditor;\n"
  },
  {
    "path": "src/client/LogKey/LogValueListEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport LogValueEditor from './LogValueEditor';\n\nclass LogValueListEditor extends React.Component {\n    constructor(props) {\n        super(props);\n        this.firstRef = React.createRef();\n    }\n\n    focus() {\n        this.firstRef.current.focus();\n    }\n\n    render() {\n        return this.props.logKeys.map((logKey, index) => (\n            <InputGroup className=\"my-1\" key={logKey.__id__}>\n                <InputGroup.Text>\n                    {logKey.name}\n                </InputGroup.Text>\n                <LogValueEditor\n                    logKey={logKey}\n                    disabled={this.props.disabled}\n                    onChange={(updatedLogKey) => {\n                        const updatedLogKeys = [...this.props.logKeys];\n                        updatedLogKeys[index] = updatedLogKey;\n                        this.props.onChange(updatedLogKeys);\n                    }}\n                    onSearch={(query) => window.api.send('value-typeahead', {\n                        source: this.props.source,\n                        query,\n                        index,\n                    })}\n                    ref={index === 0 ? this.firstRef : null}\n                />\n            </InputGroup>\n        ));\n    }\n}\n\nLogValueListEditor.propTypes = {\n    source: PropTypes.Custom.Item.isRequired,\n    logKeys: PropTypes.arrayOf(PropTypes.Custom.LogKey.isRequired).isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default LogValueListEditor;\n"
  },
  {
    "path": "src/client/LogKey/index.js",
    "content": "export { default as LogKeyEditor } from './LogKeyEditor';\nexport { default as LogKeyListEditor } from './LogKeyListEditor';\nexport { default as LogValueEditor } from './LogValueEditor';\nexport { default as LogValueListEditor } from './LogValueListEditor';\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureDetailsHeader.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    Coordinator, InputLine,\n} from '../Common';\n\nclass LogStructureDetailsHeader extends React.Component {\n    static onSearchButtonClick(logStructure) {\n        Coordinator.invoke('url-update', { search: [logStructure] });\n    }\n\n    render() {\n        const { logStructure } = this.props;\n        return (\n            <InputLine overflow styled className=\"px-2\">\n                {logStructure.logStructureGroup.name}\n                {' / '}\n                {logStructure.name}\n            </InputLine>\n        );\n    }\n}\n\nLogStructureDetailsHeader.propTypes = {\n    logStructure: PropTypes.Custom.LogStructure.isRequired,\n};\n\nexport default LogStructureDetailsHeader;\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { getPartialItem, LogStructure } from '../../common/data_types';\nimport {\n    Selector, TextInput, TypeaheadOptions, TypeaheadSelector,\n} from '../Common';\nimport { LogKeyListEditor } from '../LogKey';\nimport LogStructureFrequencyEditor from './LogStructureFrequencyEditor';\n\nconst { LogLevel } = LogStructure;\n\nclass LogStructureEditor extends React.Component {\n    constructor(props) {\n        super(props);\n        this.nameRef = React.createRef();\n    }\n\n    componentDidMount() {\n        this.nameRef.current.focus();\n    }\n\n    updateLogStructure(methodOrName, maybeValue) {\n        const updatedLogStructure = { ...this.props.logStructure };\n        if (typeof methodOrName === 'function') {\n            methodOrName(updatedLogStructure);\n        } else {\n            updatedLogStructure[methodOrName] = maybeValue;\n        }\n        LogStructure.trigger(updatedLogStructure);\n        this.props.onChange(updatedLogStructure);\n    }\n\n    renderGroup() {\n        const options = new TypeaheadOptions({\n            serverSideOptions: [{ name: 'log-structure-group' }],\n            onSelect: async (option) => window.api.send('log-structure-group-load', option),\n        });\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Group\n                </InputGroup.Text>\n                <TypeaheadSelector\n                    id=\"log-structure-editor-structure-group\"\n                    options={options}\n                    value={this.props.logStructure.logStructureGroup}\n                    disabled={this.props.disabled}\n                    onChange={(logStructureGroup) => this.updateLogStructure(\n                        'logStructureGroup',\n                        logStructureGroup,\n                    )}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderName() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Name\n                </InputGroup.Text>\n                <TextInput\n                    value={this.props.logStructure.name}\n                    disabled={this.props.disabled}\n                    onChange={(name) => this.updateLogStructure('name', name)}\n                    ref={this.nameRef}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderEventNeedsEditSelector() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Needs Edit?\n                </InputGroup.Text>\n                <Selector.Binary\n                    value={this.props.logStructure.eventNeedsEdit}\n                    disabled={this.props.disabled}\n                    onChange={(eventNeedsEdit) => this.updateLogStructure('eventNeedsEdit', eventNeedsEdit)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderEventAllowDetailsSelector() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Event Details?\n                </InputGroup.Text>\n                <Selector.Binary\n                    value={this.props.logStructure.eventAllowDetails}\n                    disabled={this.props.disabled}\n                    onChange={(eventAllowDetails) => this.updateLogStructure('eventAllowDetails', eventAllowDetails)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderLogLevelSelector() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Log Level\n                </InputGroup.Text>\n                <Selector\n                    options={LogLevel.Options}\n                    value={LogLevel.getValue(this.props.logStructure.logLevel)}\n                    disabled={this.props.disabled}\n                    onChange={(value) => this.updateLogStructure('logLevel', LogLevel.getIndex(value))}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderIsDeprecated() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Is Deprecated?\n                </InputGroup.Text>\n                <Selector.Binary\n                    value={this.props.logStructure.isDeprecated}\n                    disabled={this.props.disabled}\n                    onChange={(isDeprecated) => this.updateLogStructure('isDeprecated', isDeprecated)}\n                />\n            </InputGroup>\n        );\n    }\n\n    render() {\n        const { logStructure } = this.props;\n        return (\n            <>\n                <div className=\"my-3\">\n                    {this.renderGroup()}\n                    {this.renderName()}\n                </div>\n                <div className=\"my-3\">\n                    <LogKeyListEditor\n                        templateLabel=\"Event Title Template\"\n                        templateValue={logStructure.eventTitleTemplate}\n                        templateOptions={new TypeaheadOptions({\n                            prefixOptions: [\n                                getPartialItem(logStructure),\n                                ...logStructure.eventKeys,\n                            ],\n                            serverSideOptions: [\n                            ],\n                        })}\n                        onTemplateChange={\n                            (eventTitleTemplate) => this.updateLogStructure('eventTitleTemplate', eventTitleTemplate)\n                        }\n                        logKeys={logStructure.eventKeys}\n                        onLogKeysChange={(eventKeys) => this.updateLogStructure('eventKeys', eventKeys)}\n                        onValueSearch={(query, index) => window.api.send('value-typeahead', {\n                            logStructure: this.props.logStructure,\n                            query,\n                            index,\n                        })}\n                        disabled={this.props.disabled}\n                    />\n                    {this.renderEventNeedsEditSelector()}\n                    {this.renderEventAllowDetailsSelector()}\n                </div>\n                <div className=\"my-3\">\n                    <LogStructureFrequencyEditor\n                        logStructure={logStructure}\n                        disabled={this.props.disabled}\n                        updateLogStructure={(...args) => this.updateLogStructure(...args)}\n                    />\n                </div>\n                <div className=\"my-3\">\n                    {this.renderLogLevelSelector()}\n                    {this.renderIsDeprecated()}\n                </div>\n            </>\n        );\n    }\n}\n\nLogStructureEditor.propTypes = {\n    logStructure: PropTypes.Custom.LogStructure.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default LogStructureEditor;\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureFrequencyEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { LogStructure } from '../../common/data_types';\nimport DateUtils from '../../common/DateUtils';\nimport {\n    DateContext, DatePicker, Selector, TextInput,\n} from '../Common';\n\nconst MonthOptions = DateUtils.MonthsOfTheYear.map((month, index) => {\n    const value = `0${index + 1}`.substr(-2);\n    return { label: `${month.name} (${value})`, value };\n});\n\nconst DayOptions = Array(Math.max(...DateUtils.MonthsOfTheYear.map((month) => month.days)))\n    .fill(null)\n    .map((_, index) => {\n        const value = `0${index + 1}`.substr(-2);\n        return { label: value, value };\n    });\n\nconst WarningDayOptions = Array(15).fill(null).map((_, index) => {\n    const value = `${index}`;\n    return { label: value, value };\n});\n\nclass LogStructureFrequencyEditor extends React.Component {\n    updateIsPeriodic(newIsPeriodic) {\n        const { todayDate } = this.context;\n        this.props.updateLogStructure((updatedLogStructure) => {\n            if (newIsPeriodic) {\n                updatedLogStructure.isPeriodic = true;\n                updatedLogStructure.reminderText = updatedLogStructure._reminderText || '';\n                updatedLogStructure.frequency = (\n                    updatedLogStructure._frequency || LogStructure.Frequency.EVERYDAY\n                );\n                updatedLogStructure.warningDays = updatedLogStructure._warningDays || 0;\n                updatedLogStructure.suppressUntilDate = updatedLogStructure._suppressUntilDate || '{yesterday}';\n                DateUtils.maybeSubstitute(todayDate, updatedLogStructure, 'suppressUntilDate');\n            } else {\n                updatedLogStructure.isPeriodic = false;\n                updatedLogStructure._reminderText = updatedLogStructure.reminderText;\n                updatedLogStructure.reminderText = null;\n                updatedLogStructure._frequency = updatedLogStructure.frequency;\n                updatedLogStructure.frequency = null;\n                updatedLogStructure._warningDays = updatedLogStructure.warningDays;\n                updatedLogStructure.warningDays = null;\n                updatedLogStructure._suppressUntilDate = updatedLogStructure.suppressUntilDate;\n                updatedLogStructure.suppressUntilDate = null;\n            }\n        });\n    }\n\n    updateFrequency(newFrequency) {\n        const { todayLabel } = this.context;\n        this.props.updateLogStructure((updatedLogStructure) => {\n            const oldFrequency = updatedLogStructure.frequency;\n            updatedLogStructure.frequency = newFrequency;\n            if (newFrequency === LogStructure.Frequency.YEARLY) {\n                updatedLogStructure.frequencyArgs = (\n                    updatedLogStructure._frequencyArgs || todayLabel.substr(5)\n                );\n            } else if (oldFrequency === LogStructure.Frequency.YEARLY) {\n                updatedLogStructure._frequencyArgs = updatedLogStructure.frequencyArgs;\n                updatedLogStructure.frequencyArgs = null;\n            }\n        });\n    }\n\n    renderIsPeriodic() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Is Periodic?\n                </InputGroup.Text>\n                <Selector.Binary\n                    value={this.props.logStructure.isPeriodic}\n                    disabled={this.props.disabled}\n                    onChange={(isPeriodic) => this.updateIsPeriodic(isPeriodic)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderReminderText() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Reminder Text\n                </InputGroup.Text>\n                <TextInput\n                    value={this.props.logStructure.reminderText}\n                    disabled={this.props.disabled}\n                    onChange={(reminderText) => this.props.updateLogStructure('reminderText', reminderText)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderFrequency() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Frequency\n                </InputGroup.Text>\n                <Selector\n                    value={this.props.logStructure.frequency}\n                    options={LogStructure.Frequency.Options}\n                    disabled={this.props.disabled}\n                    onChange={(frequency) => this.updateFrequency(frequency)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderFrequencyArgs() {\n        const { frequency, frequencyArgs } = this.props.logStructure;\n        if (frequency !== LogStructure.Frequency.YEARLY) {\n            return null;\n        }\n        const [oldMonth, oldDay] = frequencyArgs.split('-');\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Yearly Date\n                </InputGroup.Text>\n                <Selector\n                    options={MonthOptions}\n                    disabled={this.props.disabled}\n                    value={oldMonth}\n                    onChange={(newMonth) => this.props.updateLogStructure('frequencyArgs', `${newMonth}-${oldDay}`)}\n                />\n                <Selector\n                    options={DayOptions}\n                    disabled={this.props.disabled}\n                    value={oldDay}\n                    onChange={(newDay) => this.props.updateLogStructure('frequencyArgs', `${oldMonth}-${newDay}`)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderWarningDays() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Warning Days\n                </InputGroup.Text>\n                <Selector\n                    options={WarningDayOptions}\n                    disabled={this.props.disabled}\n                    value={this.props.logStructure.warningDays.toString()}\n                    onChange={(warningDays) => this.props.updateLogStructure('warningDays', parseInt(warningDays, 10))}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderSuppressUntilDate() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Suppress Until\n                </InputGroup.Text>\n                <DatePicker\n                    date={this.props.logStructure.suppressUntilDate}\n                    disabled={this.props.disabled}\n                    onChange={(suppressUntilDate) => this.props.updateLogStructure('suppressUntilDate', suppressUntilDate)}\n                />\n            </InputGroup>\n        );\n    }\n\n    render() {\n        return (\n            <>\n                {this.renderIsPeriodic()}\n                {this.props.logStructure.isPeriodic\n                    ? (\n                        <>\n                            {this.renderReminderText()}\n                            {this.renderFrequency()}\n                            {this.renderFrequencyArgs()}\n                            {this.renderWarningDays()}\n                            {this.renderSuppressUntilDate()}\n                        </>\n                    )\n                    : null}\n            </>\n        );\n    }\n}\n\nLogStructureFrequencyEditor.propTypes = {\n    logStructure: PropTypes.Custom.LogStructure.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    updateLogStructure: PropTypes.func.isRequired,\n};\n\nLogStructureFrequencyEditor.contextType = DateContext;\n\nexport default LogStructureFrequencyEditor;\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureGroupEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { TextInput } from '../Common';\n\nclass LogStructureGroupEditor extends React.Component {\n    constructor(props) {\n        super(props);\n        this.nameRef = React.createRef();\n    }\n\n    componentDidMount() {\n        this.nameRef.current.focus();\n    }\n\n    updateLogStructureGroup(methodOrName, maybeValue) {\n        const updatedLogStructureGroup = { ...this.props.logStructureGroup };\n        if (typeof methodOrName === 'function') {\n            methodOrName(updatedLogStructureGroup);\n        } else {\n            updatedLogStructureGroup[methodOrName] = maybeValue;\n        }\n        this.props.onChange(updatedLogStructureGroup);\n    }\n\n    renderName() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Name\n                </InputGroup.Text>\n                <TextInput\n                    value={this.props.logStructureGroup.name}\n                    disabled={this.props.disabled}\n                    onChange={(name) => this.updateLogStructureGroup('name', name)}\n                    ref={this.nameRef}\n                />\n            </InputGroup>\n        );\n    }\n\n    render() {\n        return this.renderName();\n    }\n}\n\nLogStructureGroupEditor.propTypes = {\n    logStructureGroup: PropTypes.Custom.LogStructureGroup.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default LogStructureGroupEditor;\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureGroupList.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { BulletList } from '../Common';\nimport LogStructureGroupEditor from './LogStructureGroupEditor';\nimport LogStructureList from './LogStructureList';\n\nfunction LogStructureGroupViewer(props) {\n    const { logStructureGroup } = props;\n    return (\n        <div>{logStructureGroup.name}</div>\n    );\n}\n\nLogStructureGroupViewer.propTypes = {\n    logStructureGroup: PropTypes.Custom.LogStructureGroup.isRequired,\n};\n\nLogStructureGroupViewer.Expanded = function (props) {\n    const { logStructureGroup, ...viewerComponentProps } = props;\n    return (\n        <LogStructureList\n            where={{ logStructureGroup }}\n            allowReordering\n            viewerComponentProps={viewerComponentProps}\n        />\n    );\n};\n\nLogStructureGroupViewer.Expanded.propTypes = {\n    logStructureGroup: PropTypes.Custom.LogStructureGroup.isRequired,\n};\n\nfunction LogStructureGroupList(props) {\n    return (\n        <BulletList\n            {...props}\n            name=\"Structure Groups\"\n            dataType=\"log-structure-group\"\n            valueKey=\"logStructureGroup\"\n            ViewerComponent={LogStructureGroupViewer}\n            EditorComponent={LogStructureGroupEditor}\n            allowReordering\n        />\n    );\n}\n\nexport default LogStructureGroupList;\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureList.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    BulletList, DetailsIcon, InfoIcon, LeftRight, Link, TextEditor, WarningIcon,\n} from '../Common';\nimport LogStructureEditor from './LogStructureEditor';\n\nfunction LogStructureViewer(props) {\n    const { logStructure, showDetails } = props;\n    if (!showDetails) {\n        return (\n            <Link logStructure={logStructure}>\n                {logStructure.name}\n                <DetailsIcon isShown={!!logStructure.details} />\n                <InfoIcon isShown={!!logStructure.eventAllowDetails} />\n                <WarningIcon isShown={logStructure.isDeprecated} />\n            </Link>\n        );\n    }\n    let suffix;\n    if (logStructure.isPeriodic) {\n        if (logStructure.frequencyArgs) {\n            suffix = `(${logStructure.frequency}: ${logStructure.frequencyArgs})`;\n        } else {\n            suffix = `(${logStructure.frequency})`;\n        }\n    }\n    return (\n        <LeftRight>\n            <span>\n                <TextEditor\n                    unstyled\n                    disabled\n                    value={logStructure.eventTitleTemplate}\n                    isSingleLine\n                />\n                <DetailsIcon isShown={!!logStructure.details} />\n                <InfoIcon isShown={!!logStructure.eventAllowDetails} />\n                <WarningIcon isShown={logStructure.isDeprecated} />\n            </span>\n            {suffix}\n        </LeftRight>\n    );\n}\n\nLogStructureViewer.propTypes = {\n    logStructure: PropTypes.Custom.LogStructure.isRequired,\n    showDetails: PropTypes.bool,\n};\n\nfunction LogStructureList(props) {\n    return (\n        <BulletList\n            {...props}\n            name=\"Structures\"\n            dataType=\"log-structure\"\n            valueKey=\"logStructure\"\n            ViewerComponent={LogStructureViewer}\n            EditorComponent={LogStructureEditor}\n        />\n    );\n}\n\nLogStructureList.Single = function (props) {\n    return (\n        <BulletList.Item\n            dataType=\"log-structure\"\n            value={props.logStructure}\n            valueKey=\"logStructure\"\n            ViewerComponent={LogStructureViewer}\n            EditorComponent={LogStructureEditor}\n        />\n    );\n};\n\nLogStructureList.Single.propTypes = {\n    logStructure: PropTypes.Custom.LogStructure.isRequired,\n};\n\nexport default LogStructureList;\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureOptions.js",
    "content": "import assert from 'assert';\n\nimport { TypeaheadOptions } from '../Common';\n\nclass LogStructureOptions {\n    static get() {\n        return new TypeaheadOptions({\n            serverSideOptions: [\n                { name: 'log-structure' },\n                { name: 'log-topic' },\n            ],\n        });\n    }\n\n    static getTypeToActionMap() {\n        return {\n            'log-structure': (item, where, extra) => {\n                if (!where.__id__) where.__id__ = [];\n                where.__id__.push(item.__id__);\n                extra.searchView = true;\n            },\n            'log-topic': (item, where, extra) => {\n                if (!where.logTopics) {\n                    where.logTopics = [];\n                }\n                where.logTopics.push(item);\n                extra.searchView = true;\n            },\n        };\n    }\n\n    static extractData(items, typeToActionMap) {\n        const where = {};\n        const extra = {};\n\n        items.forEach((item) => {\n            const action = typeToActionMap[item.__type__];\n            if (action) {\n                action(item, where, extra);\n            } else {\n                assert(false, `Unable to process ${JSON.stringify(item)}`);\n            }\n        });\n        return { where, extra };\n    }\n}\n\nexport default LogStructureOptions;\n"
  },
  {
    "path": "src/client/LogStructure/LogStructureSearch.js",
    "content": "import React from 'react';\n\nimport PropTypes from '../prop-types';\nimport LogStructureGroupList from './LogStructureGroupList';\nimport LogStructureList from './LogStructureList';\nimport LogStructureOptions from './LogStructureOptions';\n\nclass LogStructureSearch extends React.Component {\n    static getTypeaheadOptions() {\n        return LogStructureOptions.get();\n    }\n\n    static getDerivedStateFromProps(props, _state) {\n        return LogStructureOptions.extractData(\n            props.search,\n            LogStructureOptions.getTypeToActionMap(),\n        );\n    }\n\n    constructor(props) {\n        super(props);\n        this.state = {};\n    }\n\n    renderSearchView() {\n        return (\n            <LogStructureList\n                name=\"Selected Structure(s)\"\n                allowCreation={false}\n                allowReordering={false}\n                where={this.state.where}\n                viewerComponentProps={{ showDetails: true }}\n            />\n        );\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    renderDefaultView() {\n        return (\n            <LogStructureGroupList\n                where={{}}\n                viewerComponentProps={{ showDetails: true }}\n            />\n        );\n    }\n\n    render() {\n        if (this.state.extra.searchView) {\n            return this.renderSearchView();\n        }\n        return this.renderDefaultView();\n    }\n}\n\nLogStructureSearch.propTypes = {\n    search: PropTypes.arrayOf(PropTypes.Custom.Item.isRequired).isRequired,\n};\n\nexport default LogStructureSearch;\n"
  },
  {
    "path": "src/client/LogStructure/index.js",
    "content": "export { default as LogStructureEditor } from './LogStructureEditor';\nexport { default as LogStructureGroupList } from './LogStructureGroupList';\nexport { default as LogStructureList } from './LogStructureList';\nexport { default as LogStructureSearch } from './LogStructureSearch';\nexport { default as LogStructureDetailsHeader } from './LogStructureDetailsHeader';\n"
  },
  {
    "path": "src/client/LogTopic/LogTopicDetailsHeader.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    Coordinator, Dropdown, InputLine, Link,\n} from '../Common';\nimport LogTopicOptions from './LogTopicOptions';\n\nclass LogTopicDetailsHeader extends React.Component {\n    static onSearchButtonClick(logTopic) {\n        Coordinator.invoke('url-update', { search: [logTopic] });\n    }\n\n    renderParentTopic() {\n        const { logTopic } = this.props;\n        if (!logTopic.parentLogTopic) {\n            return null;\n        }\n        return (\n            <>\n                <Link logTopic={logTopic.parentLogTopic}>\n                    {logTopic.parentLogTopic.name}\n                </Link>\n                {' / '}\n            </>\n        );\n    }\n\n    renderChildTopics() {\n        const { logTopic } = this.props;\n        if (logTopic.hasStructure) {\n            return null;\n        }\n        return (\n            <>\n                {' / '}\n                <Dropdown\n                    disabled={false}\n                    options={LogTopicOptions.get({ allowCreation: true, parentLogTopic: logTopic })}\n                    onChange={(childLogTopic) => Coordinator.invoke(\n                        'url-update',\n                        { details: childLogTopic },\n                    )}\n                >\n                    <a href=\"#\" className=\"topic\">...</a>\n                </Dropdown>\n            </>\n        );\n    }\n\n    render() {\n        const { logTopic } = this.props;\n        return (\n            <InputLine overflow styled className=\"px-2\">\n                {this.renderParentTopic()}\n                {logTopic.name}\n                {this.renderChildTopics()}\n            </InputLine>\n        );\n    }\n}\n\nLogTopicDetailsHeader.propTypes = {\n    logTopic: PropTypes.Custom.LogTopic.isRequired,\n};\n\nexport default LogTopicDetailsHeader;\n"
  },
  {
    "path": "src/client/LogTopic/LogTopicEditor.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport { LogTopic } from '../../common/data_types';\nimport {\n    Selector, TextInput, TypeaheadOptions, TypeaheadSelector,\n} from '../Common';\nimport { LogKeyListEditor, LogValueListEditor } from '../LogKey';\n\nclass LogTopicEditor extends React.Component {\n    constructor(props) {\n        super(props);\n        this.nameRef = React.createRef();\n    }\n\n    componentDidMount() {\n        this.nameRef.current.focus();\n    }\n\n    updateLogTopic(methodOrName, maybeValue) {\n        const updatedLogTopic = { ...this.props.logTopic };\n        if (typeof methodOrName === 'function') {\n            methodOrName(updatedLogTopic);\n        } else {\n            updatedLogTopic[methodOrName] = maybeValue;\n        }\n        LogTopic.trigger(updatedLogTopic);\n        this.props.onChange(updatedLogTopic);\n    }\n\n    renderParent() {\n        const options = new TypeaheadOptions({\n            serverSideOptions: [{ name: 'log-topic' }],\n            onSelect: async (option) => window.api.send('log-topic-load', option),\n        });\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Parent\n                </InputGroup.Text>\n                <TypeaheadSelector\n                    id=\"log-topic-editor-parent-topic\"\n                    options={options}\n                    value={this.props.logTopic.parentLogTopic}\n                    disabled={this.props.disabled}\n                    onChange={(parentLogTopic) => this.updateLogTopic('parentLogTopic', parentLogTopic)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderName() {\n        const { parentLogTopic } = this.props.logTopic;\n        const isNameDerived = parentLogTopic ? parentLogTopic.childNameTemplate !== null : false;\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Name\n                </InputGroup.Text>\n                <TextInput\n                    value={this.props.logTopic.name}\n                    disabled={this.props.disabled || isNameDerived}\n                    onChange={(name) => this.updateLogTopic('name', name)}\n                    ref={this.nameRef}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderIsDeprecated() {\n        return (\n            <InputGroup className=\"my-1\">\n                <InputGroup.Text>\n                    Is Deprecated?\n                </InputGroup.Text>\n                <Selector.Binary\n                    value={this.props.logTopic.isDeprecated}\n                    disabled={this.props.disabled}\n                    onChange={(isDeprecated) => this.updateLogTopic('isDeprecated', isDeprecated)}\n                />\n            </InputGroup>\n        );\n    }\n\n    renderValues() {\n        const { parentLogTopic } = this.props.logTopic;\n        if (!parentLogTopic || !parentLogTopic.childKeys) {\n            return null;\n        }\n        return (\n            <LogValueListEditor\n                source={parentLogTopic}\n                logKeys={parentLogTopic.childKeys}\n                disabled={this.props.disabled}\n                onChange={(updatedChildKeys) => this.updateLogTopic((updatedLogTopic) => {\n                    updatedLogTopic.parentLogTopic.childKeys = updatedChildKeys;\n                })}\n            />\n        );\n    }\n\n    renderChildKeys() {\n        const { logTopic } = this.props;\n        let logKeyList;\n        if (logTopic.childKeys) {\n            logKeyList = (\n                <LogKeyListEditor\n                    templateLabel=\"Child Name Template\"\n                    templateValue={logTopic.childNameTemplate}\n                    templateOptions={new TypeaheadOptions({\n                        prefixOptions: logTopic.childKeys,\n                        serverSideOptions: [],\n                    })}\n                    onTemplateChange={(childNameTemplate) => this.updateLogTopic('childNameTemplate', childNameTemplate)}\n                    logKeys={logTopic.childKeys || []}\n                    onLogKeysChange={(newChildKeys) => this.updateLogTopic('childKeys', newChildKeys)}\n                    onValueSearch={(query, index) => { throw new Error('not implemented'); }}\n                    disabled={this.props.disabled}\n                />\n            );\n        }\n        return (\n            <>\n                <InputGroup className=\"my-1\">\n                    <InputGroup.Text>\n                        Enable Child Keys?\n                    </InputGroup.Text>\n                    <Selector.Binary\n                        value={!!logTopic.childKeys}\n                        disabled={this.props.disabled}\n                        onChange={(enableChildKeys) => this.updateLogTopic((updatedLogTopic) => {\n                            if (!enableChildKeys) {\n                                updatedLogTopic._childKeys = updatedLogTopic.childKeys;\n                                updatedLogTopic.childKeys = null;\n                            } else {\n                                updatedLogTopic.childKeys = updatedLogTopic._childKeys || [];\n                            }\n                        })}\n                    />\n                </InputGroup>\n                {logKeyList}\n            </>\n        );\n    }\n\n    render() {\n        return (\n            <>\n                <div className=\"my-3\">\n                    {this.renderParent()}\n                    {this.renderValues()}\n                </div>\n                <div className=\"my-3\">\n                    {this.renderName()}\n                </div>\n                <div className=\"my-3\">\n                    {this.renderIsDeprecated()}\n                </div>\n                <div className=\"my-3\">\n                    {this.renderChildKeys()}\n                </div>\n            </>\n        );\n    }\n}\n\nLogTopicEditor.propTypes = {\n    logTopic: PropTypes.Custom.LogTopic.isRequired,\n    disabled: PropTypes.bool.isRequired,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default LogTopicEditor;\n"
  },
  {
    "path": "src/client/LogTopic/LogTopicList.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport {\n    BulletList, DetailsIcon, Link, WarningIcon,\n} from '../Common';\nimport LogTopicEditor from './LogTopicEditor';\n\nfunction LogTopicViewer(props) {\n    const { logTopic } = props;\n    let childIndicator = null;\n    if (logTopic.childCount) {\n        childIndicator = (\n            <span className=\"ml-1\" style={{ color: 'var(--link-color)' }}>\n                {logTopic.childCount}\n            </span>\n        );\n    }\n    return (\n        <span>\n            <Link logTopic={logTopic}>\n                {logTopic.name}\n            </Link>\n            <DetailsIcon isShown={!!logTopic.details} />\n            {childIndicator}\n            <WarningIcon isShown={logTopic.isDeprecated} />\n        </span>\n    );\n}\n\nLogTopicViewer.propTypes = {\n    logTopic: PropTypes.Custom.LogTopic.isRequired,\n};\n\nfunction LogTopicList(props) {\n    return (\n        <BulletList\n            name=\"Topics\"\n            dataType=\"log-topic\"\n            valueKey=\"logTopic\"\n            ViewerComponent={LogTopicViewer}\n            EditorComponent={LogTopicEditor}\n            allowReordering\n            allowSorting\n            {...props}\n        />\n    );\n}\n\nLogTopicViewer.Expanded = function (props) {\n    const { logTopic } = props;\n    if (logTopic.hasStructure) {\n        return null;\n    }\n    return (\n        <LogTopicList\n            name=\"Sub Topics\"\n            where={{ parentLogTopic: logTopic }}\n        />\n    );\n};\n\nLogTopicViewer.Expanded.propTypes = {\n    logTopic: PropTypes.Custom.LogTopic.isRequired,\n};\n\nLogTopicList.Single = function (props) {\n    return (\n        <BulletList.Item\n            dataType=\"log-topic\"\n            value={props.logTopic}\n            valueKey=\"logTopic\"\n            ViewerComponent={LogTopicViewer}\n            EditorComponent={LogTopicEditor}\n        />\n    );\n};\n\nLogTopicList.Single.propTypes = {\n    logTopic: PropTypes.Custom.LogTopic.isRequired,\n};\n\nexport default LogTopicList;\n"
  },
  {
    "path": "src/client/LogTopic/LogTopicOptions.js",
    "content": "import assert from 'assert';\n\nimport { getVirtualID, isRealItem, LogTopic } from '../../common/data_types';\nimport { Coordinator, TypeaheadOptions } from '../Common';\nimport LogTopicEditor from './LogTopicEditor';\n\nconst CREATE_ITEM = {\n    __type__: 'log-topic',\n    __id__: getVirtualID(),\n    name: 'Create New Topic ...',\n    getItem(_option, partialParentLogTopic) {\n        return window.api.send('log-topic-load', partialParentLogTopic)\n            .then((parentLogTopic) => new Promise((resolve) => {\n                Coordinator.invoke('modal-editor', {\n                    dataType: 'log-topic',\n                    EditorComponent: LogTopicEditor,\n                    valueKey: 'logTopic',\n                    value: LogTopic.createVirtual({ parentLogTopic }),\n                    onClose: (newLogTopic) => {\n                        if (newLogTopic && isRealItem(newLogTopic)) {\n                            resolve(newLogTopic);\n                        } else {\n                            resolve(null);\n                        }\n                    },\n                });\n            }));\n    },\n};\n\nclass LogTopicOptions {\n    static get({\n        allowCreation, parentLogTopic, beforeSelect, afterSelect,\n    } = {}) {\n        return new TypeaheadOptions({\n            serverSideOptions: [{ name: 'log-topic', where: { parentLogTopic } }],\n            suffixOptions: [allowCreation ? CREATE_ITEM : null].filter((item) => !!item),\n            onSelect: async (option) => {\n                if (option.getItem) {\n                    if (beforeSelect) beforeSelect();\n                    const result = await option.getItem(option, parentLogTopic);\n                    if (afterSelect) afterSelect();\n                    return result;\n                }\n                return undefined;\n            },\n        });\n    }\n\n    static getTypeToActionMap() {\n        return {\n            'log-topic': (item, where, extra) => {\n                if (!where.logTopics) {\n                    where.logTopics = [];\n                }\n                where.logTopics.push(item);\n                extra.searchView = true;\n            },\n        };\n    }\n\n    static extractData(items, typeToActionMap) {\n        const where = {};\n        const extra = {};\n\n        items.forEach((item) => {\n            const action = typeToActionMap[item.__type__];\n            if (action) {\n                action(item, where, extra);\n            } else {\n                assert(false, `Unable to process ${JSON.stringify(item)}`);\n            }\n        });\n        return { where, extra };\n    }\n}\n\nexport default LogTopicOptions;\n"
  },
  {
    "path": "src/client/LogTopic/LogTopicSearch.js",
    "content": "import React from 'react';\n\nimport { TypeaheadOptions } from '../Common';\nimport PropTypes from '../prop-types';\nimport LogTopicList from './LogTopicList';\nimport LogTopicOptions from './LogTopicOptions';\n\nclass LogTopicSearch extends React.Component {\n    static getTypeaheadOptions() {\n        const where = {};\n        return new TypeaheadOptions({\n            serverSideOptions: [\n                { name: 'log-topic', args: { where } },\n            ],\n        });\n    }\n\n    static getDerivedStateFromProps(props, _state) {\n        return LogTopicOptions.extractData(\n            props.search,\n            LogTopicOptions.getTypeToActionMap(),\n        );\n    }\n\n    constructor(props) {\n        super(props);\n        this.state = {};\n    }\n\n    renderSearchView() {\n        return (\n            <>\n                <LogTopicList\n                    name=\"Selected Topic\"\n                    where={{\n                        __id__: this.state.where.logTopics\n                            .map((logTopic) => logTopic.__id__),\n                    }}\n                    allowCreation={false}\n                    allowReordering={false}\n                />\n                <LogTopicList\n                    name=\"Referencing Topics\"\n                    where={this.state.where}\n                    allowCreation={false}\n                    allowReordering={false}\n                />\n            </>\n        );\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    renderDefaultView() {\n        return <LogTopicList where={{ parentLogTopic: null }} />;\n    }\n\n    render() {\n        if (this.state.extra.searchView) {\n            return this.renderSearchView();\n        }\n        return this.renderDefaultView();\n    }\n}\n\nLogTopicSearch.propTypes = {\n    search: PropTypes.arrayOf(PropTypes.Custom.Item.isRequired).isRequired,\n};\n\nexport default LogTopicSearch;\n"
  },
  {
    "path": "src/client/LogTopic/index.js",
    "content": "export { default as LogTopicList } from './LogTopicList';\nexport { default as LogTopicEditor } from './LogTopicEditor';\nexport { default as LogTopicSearch } from './LogTopicSearch';\nexport { default as LogTopicOptions } from './LogTopicOptions';\nexport { default as LogTopicDetailsHeader } from './LogTopicDetailsHeader';\n"
  },
  {
    "path": "src/client/Reminders/ReminderItem.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport Form from 'react-bootstrap/Form';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { BsList } from 'react-icons/bs';\n\nimport { LogEvent } from '../../common/data_types';\nimport {\n    Coordinator, DateContext, Dropdown, Highlightable, Icon, InputLine,\n} from '../Common';\nimport { LogEventEditor } from '../LogEvent';\nimport { LogStructureEditor } from '../LogStructure';\n\nclass ReminderItem extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { isHighlighted: false };\n        this.dropdownRef = React.createRef();\n    }\n\n    onEditButtonClick() {\n        Coordinator.invoke('modal-editor', {\n            dataType: 'log-structure',\n            EditorComponent: LogStructureEditor,\n            valueKey: 'logStructure',\n            value: this.props.logStructure,\n        });\n    }\n\n    onCompleteReminder(logEvent = null) {\n        const { todayLabel } = this.context;\n        const { logStructure } = this.props;\n        const wasLogEventProvided = !!logEvent;\n        if (!logEvent) {\n            logEvent = LogEvent.createVirtual({ date: todayLabel, logStructure });\n        }\n        if (logStructure.eventNeedsEdit && !wasLogEventProvided) {\n            // This modal is only closed after the reminder-complete RPC.\n            this.closeModal = Coordinator.invoke('modal-editor', {\n                dataType: 'log-event',\n                EditorComponent: LogEventEditor,\n                valueKey: 'logEvent',\n                value: logEvent,\n                onSave: (updatedLogEvent) => this.onCompleteReminder(updatedLogEvent),\n            });\n            return;\n        }\n        window.api.send('reminder-complete', { logStructure, logEvent, todayLabel })\n            .then(() => {\n                if (this.closeModal) {\n                    this.closeModal();\n                    delete this.closeModal;\n                }\n            });\n    }\n\n    onDismissReminder() {\n        const { todayLabel } = this.context;\n        const { logStructure } = this.props;\n        window.api.send('reminder-dismiss', { logStructure, todayLabel });\n    }\n\n    renderRight() {\n        const { logStructure } = this.props;\n        if (!this.state.isHighlighted) {\n            return (\n                <span className=\"float-right\">\n                    {logStructure.reminderScore.value}\n                </span>\n            );\n        }\n        const actions = [\n            {\n                __id__: 'done',\n                name: 'Mark as Complete',\n                perform: (_event) => this.onCompleteReminder(),\n            },\n            {\n                __id__: 'dismiss',\n                name: 'Dismiss Reminder',\n                perform: (_event) => this.onDismissReminder(),\n            },\n            {\n                __id__: 'edit',\n                name: 'Edit Structure',\n                perform: (_event) => this.onEditButtonClick(),\n            },\n            {\n                __id__: 'info',\n                name: 'Debug Info',\n                perform: (_event) => Coordinator.invoke(\n                    'modal-error',\n                    JSON.stringify(logStructure, null, 4),\n                ),\n            },\n        ];\n        return (\n            <Icon className=\"ml-1\" title=\"Actions\">\n                <Dropdown\n                    disabled={false}\n                    options={actions}\n                    onChange={(action, event) => action.perform(event)}\n                    ref={this.dropdownRef}\n                >\n                    <BsList\n                        onMouseOver={() => {\n                            if (this.dropdownRef.current) {\n                                this.dropdownRef.current.show();\n                            }\n                        }}\n                    />\n                </Dropdown>\n            </Icon>\n        );\n    }\n\n    render() {\n        const { logStructure } = this.props;\n        return (\n            <Highlightable\n                key={logStructure.__id__}\n                isHighlighted={this.state.isHighlighted}\n                onChange={(isHighlighted) => this.setState({ isHighlighted })}\n            >\n                <InputGroup className=\"reminder-item\">\n                    <Form.Check\n                        type=\"checkbox\"\n                        inline\n                        checked={false}\n                        readOnly\n                        onClick={(event) => {\n                            if (event.shiftKey) {\n                                this.onDismissReminder();\n                            } else {\n                                this.onCompleteReminder();\n                            }\n                        }}\n                        style={{ marginRight: 'none' }}\n                        tabIndex={-1}\n                    />\n                    <InputLine styled={false}>\n                        {logStructure.reminderText || logStructure.name}\n                    </InputLine>\n                    {this.renderRight()}\n                </InputGroup>\n            </Highlightable>\n        );\n    }\n}\n\nReminderItem.propTypes = {\n    logStructure: PropTypes.Custom.LogStructure.isRequired,\n};\n\nReminderItem.contextType = DateContext;\n\nexport default ReminderItem;\n"
  },
  {
    "path": "src/client/Reminders/ReminderList.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { SidebarSection } from '../Common';\nimport ReminderItem from './ReminderItem';\n\nclass ReminderList extends React.Component {\n    renderContent() {\n        if (this.props.logStructures.length === 0) {\n            return <div className=\"ml-3\">All done for now!</div>;\n        }\n        return this.props.logStructures.map((logStructure) => (\n            <ReminderItem\n                key={logStructure.__id__}\n                logStructure={logStructure}\n            />\n        ));\n    }\n\n    render() {\n        return (\n            <SidebarSection title={this.props.name}>\n                {this.renderContent()}\n            </SidebarSection>\n        );\n    }\n}\n\nReminderList.propTypes = {\n    name: PropTypes.string.isRequired,\n    logStructures: PropTypes.arrayOf(PropTypes.Custom.LogStructure.isRequired).isRequired,\n};\n\nexport default ReminderList;\n"
  },
  {
    "path": "src/client/Reminders/ReminderSidebar.js",
    "content": "import React from 'react';\n\nimport { DataLoader, DateContext } from '../Common';\nimport PropTypes from '../prop-types';\nimport ReminderList from './ReminderList';\n\nclass ReminderSidebar extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { logStructureGroups: null };\n    }\n\n    componentDidMount() {\n        const { todayLabel } = this.props;\n        this.dataLoader = new DataLoader({\n            getInput: () => ({ name: 'reminder-sidebar', args: { todayLabel } }),\n            onData: (logStructureGroups) => this.setState({ logStructureGroups }),\n        });\n    }\n\n    componentDidUpdate() {\n        this.dataLoader.reload();\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    render() {\n        if (this.state.logStructureGroups === null) {\n            return 'Loading ...';\n        }\n        return this.state.logStructureGroups.map((reminderGroup) => (\n            <ReminderList\n                key={reminderGroup.__id__}\n                name={`${reminderGroup.name} (${reminderGroup.logStructures.length})`}\n                logStructures={reminderGroup.logStructures}\n                disabled={this.props.disabled}\n            />\n        ));\n    }\n}\n\nReminderSidebar.propTypes = {\n    todayLabel: PropTypes.string.isRequired,\n    disabled: PropTypes.bool.isRequired,\n};\n\nexport default DateContext.Wrapper(ReminderSidebar);\n"
  },
  {
    "path": "src/client/Reminders/index.js",
    "content": "// eslint-disable-next-line import/prefer-default-export\nexport { default as ReminderSidebar } from './ReminderSidebar';\n"
  },
  {
    "path": "src/client/Settings/SettingsEditor.js",
    "content": "import assert from 'assert';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport {\n    HelpIcon, Selector, TextInput, TooltipElement,\n} from '../Common';\nimport PropTypes from '../prop-types';\n\nconst SETTINGS_ITEMS = [\n    {\n        key: 'display_overdue_and_upcoming_events',\n        label: 'Display Overdue And Upcoming Events',\n        type: 'boolean',\n    },\n    {\n        key: 'display_settings_section',\n        label: 'Display Settings Section',\n        type: 'boolean',\n    },\n    {\n        key: 'display_two_details_sections',\n        label: 'Display Two Details Sections',\n        type: 'boolean',\n    },\n    {\n        key: 'today_offset_hours',\n        label: 'Today Offset Hours',\n        type: 'integer',\n        description: (\n            'Adjust the time at which the day starts / ends. Eg - If you frequently stay awake until 2am or so, '\n            + 'you can set this value to \"3\", so the app does not move on to the next day until 3am.'\n        ),\n    },\n    {\n        key: 'bullet_list_page_size',\n        label: 'Bullet List Page Size',\n        type: 'integer',\n    },\n];\n\nclass SettingsEditor extends React.Component {\n    getSetting(key, defaultValue = null) {\n        return this.props.settings[key] || defaultValue;\n    }\n\n    setSetting(key, value) {\n        const settings = { ...this.props.settings };\n        settings[key] = value;\n        this.props.onChange(settings);\n    }\n\n    renderSettingsItems() {\n        return SETTINGS_ITEMS.map((item) => {\n            let inputElement = null;\n            if (item.type === 'boolean') {\n                inputElement = (\n                    <Selector.Binary\n                        disabled={this.props.disabled}\n                        value={this.getSetting(item.key, false)}\n                        onChange={(value) => this.setSetting(item.key, value)}\n                    />\n                );\n            } if (item.type === 'integer') {\n                inputElement = (\n                    <TextInput\n                        disabled={this.props.disabled}\n                        value={this.getSetting(item.key, '')}\n                        onChange={(value) => this.setSetting(item.key, value)}\n                    />\n                );\n            }\n            assert(inputElement, `unknown settings item type: ${item.type}`);\n            let tooltip;\n            if (item.description) {\n                tooltip = (\n                    <TooltipElement>\n                        <HelpIcon isShown />\n                        <span>{item.description}</span>\n                    </TooltipElement>\n                );\n            }\n            return (\n                <InputGroup className=\"my-1\" key={item.key}>\n                    <div className=\"pr-2\" style={{ width: '250px', textAlign: 'right' }}>\n                        {item.label}\n                        {tooltip}\n                    </div>\n                    {inputElement}\n                </InputGroup>\n            );\n        });\n    }\n\n    render() {\n        const results = [\n            <div key=\"my-3\">\n                {this.renderSettingsItems()}\n            </div>,\n        ];\n        Object.entries(this.props.plugins).forEach(([name, api]) => {\n            const key = api.getSettingsKey();\n            if (key === null) {\n                return;\n            }\n            const props = {\n                disabled: this.props.disabled,\n                value: this.getSetting(key),\n                onChange: (newValue) => this.setSetting(key, newValue),\n            };\n            results.push(<div key={name}>{api.getSettingsComponent(props)}</div>);\n        });\n        return results;\n    }\n}\n\nSettingsEditor.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    settings: PropTypes.objectOf(PropTypes.any.isRequired).isRequired,\n    plugins: PropTypes.Custom.Plugins.isRequired,\n    onChange: PropTypes.func.isRequired,\n    disabled: PropTypes.bool.isRequired,\n};\n\nexport default SettingsEditor;\n"
  },
  {
    "path": "src/client/Settings/SettingsModal.js",
    "content": "import React from 'react';\nimport Button from 'react-bootstrap/Button';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport Modal from 'react-bootstrap/Modal';\n\nimport { LeftRight } from '../Common';\nimport { suppressUnlessShiftKey } from '../Common/Utils';\nimport PropTypes from '../prop-types';\nimport SettingsEditor from './SettingsEditor';\n\nclass SettingsModal extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            settings: props.settings,\n            isSaving: false,\n        };\n    }\n\n    onSave() {\n        this.setState({ isSaving: true });\n        window.api.send('settings-set', this.state.settings)\n            .then(() => this.setState({ isSaving: false }));\n    }\n\n    render() {\n        return (\n            <Modal\n                show={this.props.isShown}\n                onHide={() => {\n                    this.setState({ settings: this.props.settings });\n                    this.props.onClose();\n                }}\n                onEscapeKeyDown={suppressUnlessShiftKey}\n            >\n                <Modal.Header closeButton>\n                    <Modal.Title>Settings</Modal.Title>\n                </Modal.Header>\n                <Modal.Body>\n                    <SettingsEditor\n                        settings={this.state.settings}\n                        plugins={this.props.plugins}\n                        disabled={this.state.isSaving}\n                        onChange={(settings) => this.setState({ settings })}\n                    />\n                </Modal.Body>\n                <Modal.Body>\n                    <LeftRight>\n                        <div />\n                        <InputGroup>\n                            <Button\n                                disabled={this.state.isSaving}\n                                onClick={() => this.onSave()}\n                                style={{ width: '50px' }}\n                            >\n                                Save\n                            </Button>\n                        </InputGroup>\n                    </LeftRight>\n                </Modal.Body>\n            </Modal>\n        );\n    }\n}\n\nSettingsModal.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    settings: PropTypes.objectOf(PropTypes.any.isRequired).isRequired,\n    plugins: PropTypes.Custom.Plugins.isRequired,\n    isShown: PropTypes.bool.isRequired,\n    onClose: PropTypes.func.isRequired,\n};\n\nexport default SettingsModal;\n"
  },
  {
    "path": "src/client/Settings/SettingsSection.js",
    "content": "import React from 'react';\n\nimport { LeftRight, SettingsContext, SidebarSection } from '../Common';\nimport PropTypes from '../prop-types';\nimport SettingsModal from './SettingsModal';\n\nclass SettingsSection extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {\n            isShown: false,\n        };\n    }\n\n    componentDidMount() {\n        window.onkeydown = (event) => {\n            if (event.shiftKey && event.metaKey && event.key === 's') {\n                this.setState({ isShown: true });\n            }\n        };\n    }\n\n    componentWillUnmount() {\n        delete window.onkeydown;\n    }\n\n    render() {\n        const settings = this.context;\n        const settingsSection = settings.display_settings_section ? (\n            <SidebarSection>\n                <LeftRight>\n                    <a href=\"#\" onClick={() => this.setState({ isShown: true })}>Settings</a>\n                    <span />\n                </LeftRight>\n            </SidebarSection>\n        ) : null;\n        return (\n            <>\n                <SettingsModal\n                    settings={this.props.settings}\n                    plugins={this.props.plugins}\n                    isShown={this.state.isShown}\n                    onClose={() => this.setState({ isShown: false })}\n                />\n                {settingsSection}\n            </>\n        );\n    }\n}\n\nSettingsSection.propTypes = {\n    // eslint-disable-next-line react/forbid-prop-types\n    settings: PropTypes.objectOf(PropTypes.any.isRequired).isRequired,\n    plugins: PropTypes.Custom.Plugins.isRequired,\n};\n\nSettingsSection.contextType = SettingsContext;\n\nexport default SettingsSection;\n"
  },
  {
    "path": "src/client/Settings/index.js",
    "content": "// eslint-disable-next-line import/prefer-default-export\nexport { default as SettingsSection } from './SettingsSection';\n"
  },
  {
    "path": "src/client/__tests__/Colors.test.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst walkSync = require('walk-sync');\n\ntest('verify_no_random_colors', async () => {\n    const rootPath = 'src/';\n    const excludeNames = ['index.css'];\n    let excludeCount = 0;\n    walkSync(rootPath, ['**/*.css']).forEach((fileName) => {\n        const filePath = path.join(rootPath, fileName);\n        if (excludeNames.some((name) => filePath.endsWith(name))) {\n            excludeCount += 1;\n        } else {\n            const fileData = fs.readFileSync(filePath).toString();\n            expect(fileData.includes('#')).toBeFalsy();\n        }\n    });\n    expect(excludeCount).toEqual(excludeNames.length);\n});\n"
  },
  {
    "path": "src/client/index.css",
    "content": "body {\n    --background-color: #111;\n    --component-color: #222;\n    --component-highlight-color: #333;\n    --text-color: white;\n    --text-disabled-color: #9197a3;\n    --link-color: #3498DB;\n    --topic-color: #00bc8c;\n    --input-text-color: #fff;\n    --input-background-color: #333;\n    --input-disabled-background-color: #222;\n    --suggestion-highlight-color: #375a7f;\n    --input-background-token-color: #444;\n    --warning-color: #ff5281;\n\n    --font-size: 13px;\n    --font-family: \"Lato\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n\n    background-color: var(--background-color);\n    color: var(--text-color);\n    font-size: var(--font-size);\n    font-family: var(--font-family);\n}\n\na,\na:hover {\n    color: var(--link-color);\n    cursor: pointer;\n}\n\na.topic,\na.topic:hover {\n    color: var(--topic-color);\n}\n\npre {\n    color: var(--text-color);\n}\n\n.float-left {\n    float: left;\n}\n\n.float-right {\n    float: right;\n}\n\n.monospace {\n    font-family: Consolas;\n}\n"
  },
  {
    "path": "src/client/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>GLADOS</title>\n  </head>\n  <body onLoad=\"main()\">\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/client/index.js",
    "content": "import './Bootstrap';\nimport './index.css';\nimport './prop-types'; // Load PropTypes.Custom\n\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport io from 'socket.io-client';\n\nimport { isVirtualItem } from '../common/data_types';\nimport SocketRPC from '../common/SocketRPC';\nimport { Application } from './Application';\nimport { Coordinator } from './Common';\n\nfunction getCookies() {\n    const cookies = {};\n    document.cookie.split('; ').forEach((item) => {\n        const [key, value] = item.split('=');\n        cookies[key] = decodeURIComponent(value);\n    });\n    return cookies;\n}\n\nwindow.main = function main() {\n    const cookies = getCookies();\n    window.api = SocketRPC.client(\n        io(`${cookies.host}:${cookies.port}`),\n        (name, input, output) => {\n            // The \"log-event-created\" event is used to auto-add the \"Log Level: Minor+\"\n            // item to LogEventSearch typeahead, to make sure that the new item is visible.\n            if (name === 'log-event-upsert' && isVirtualItem(input)) {\n                Coordinator.broadcast('log-event-created', output);\n            } else if (name === 'reminder-complete') {\n                Coordinator.broadcast('log-event-created', output.logEvent);\n            }\n        },\n        // TODO: Eliminate this catch-all.\n        (_name, _input, error) => Coordinator.invoke('modal-error', error),\n    );\n\n    const plugins = {};\n    const pluginsContext = require.context('../plugins', true, /client\\.js$/);\n    const pluginPatterns = JSON.parse(cookies.plugins).map((pattern) => new RegExp(pattern));\n    pluginsContext.keys()\n        .filter((filePath) => pluginPatterns.some((regex) => filePath.match(regex)))\n        .forEach((filePath) => {\n            const exports = pluginsContext(filePath);\n            const name = filePath.split('/').slice(1, -1).join('/');\n            plugins[name] = exports.default;\n        });\n\n    ReactDOM.render(<Application plugins={plugins} />, document.getElementById('root'));\n};\n"
  },
  {
    "path": "src/client/prop-types.js",
    "content": "import PropTypes from 'prop-types';\n\nconst DateRange = PropTypes.shape({\n    startDate: PropTypes.string.isRequired,\n    endDate: PropTypes.string.isRequired,\n});\n\nconst EnumOptions = PropTypes.arrayOf(\n    PropTypes.shape({\n        value: PropTypes.string.isRequired,\n        label: PropTypes.string.isRequired,\n    }).isRequired,\n);\n\nconst Item = PropTypes.shape({\n    __type__: PropTypes.string.isRequired,\n    __id__: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n});\n\nconst LogTopic = PropTypes.shape({\n    __id__: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n});\n\nconst LogKey = PropTypes.shape({\n    name: PropTypes.string.isRequired,\n    type: PropTypes.string.isRequired,\n    isOptional: PropTypes.bool,\n    parentTopic: LogTopic,\n});\n\nconst LogStructureGroup = PropTypes.shape({\n    __id__: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n});\n\nconst LogStructure = PropTypes.shape({\n    __id__: PropTypes.number.isRequired,\n    name: PropTypes.string.isRequired,\n    eventKeys: PropTypes.arrayOf(LogKey.isRequired).isRequired,\n});\n\nconst LogEvent = PropTypes.shape({\n    __id__: PropTypes.number.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    title: PropTypes.object,\n    logStructure: LogStructure,\n});\n\nconst Plugins = PropTypes.objectOf(\n    PropTypes.func.isRequired,\n);\n\nPropTypes.Custom = {\n    DateRange,\n    EnumOptions,\n    Item,\n    LogTopic,\n    LogStructureGroup,\n    LogKey,\n    LogStructure,\n    LogEvent,\n    Plugins,\n};\n\nexport default PropTypes;\n"
  },
  {
    "path": "src/common/AsyncUtils.js",
    "content": "export function asyncSequence(items, method) {\n    if (!items) {\n        return Promise.resolve();\n    }\n    return new Promise((resolve, reject) => {\n        let index = 0;\n        const results = [];\n        const next = () => {\n            // console.info(index, 'of', items.length);\n            if (index === items.length) {\n                resolve(results);\n            } else {\n                method(items[index], index, items)\n                    .then((result) => {\n                        results.push(result);\n                        index += 1;\n                        next();\n                    })\n                    .catch((error) => reject(error));\n            }\n        };\n        next();\n    });\n}\n\nexport function asyncFilter(items, method) {\n    return new Promise((resolve, reject) => {\n        Promise.all(items.map((item) => method(item)))\n            .then((decisions) => {\n                const results = [];\n                decisions.forEach((decision, index) => {\n                    if (decision) {\n                        results.push(items[index]);\n                    }\n                });\n                resolve(results);\n            })\n            .catch(reject);\n    });\n}\n\nexport function callbackToPromise(method, ...args) {\n    return new Promise((resolve, reject) => {\n        method(...args, (error, result) => {\n            if (error) {\n                reject(error);\n            } else {\n                resolve(result);\n            }\n        });\n    });\n}\n"
  },
  {
    "path": "src/common/DateUtils.js",
    "content": "import assert from 'assert';\nimport {\n    addDays, format, isValid, parse, set, subDays,\n} from 'date-fns';\n\nconst MS_IN_HOURS = 60 * 60 * 1000;\nconst LABEL_FORMAT = 'yyyy-MM-dd';\nconst MonthsOfTheYear = [\n    { name: 'January', days: 31 },\n    { name: 'February', days: 29 },\n    { name: 'March', days: 31 },\n    { name: 'April', days: 30 },\n    { name: 'May', days: 31 },\n    { name: 'June', days: 30 },\n    { name: 'July', days: 31 },\n    { name: 'August', days: 31 },\n    { name: 'September', days: 30 },\n    { name: 'October', days: 31 },\n    { name: 'November', days: 30 },\n    { name: 'December', days: 31 },\n];\nconst DaysOfTheWeek = [\n    'Sunday',\n    'Monday',\n    'Tuesday',\n    'Wednesday',\n    'Thursday',\n    'Friday',\n    'Saturday',\n];\nconst timeValues = {\n    hours: 0,\n    minutes: 0,\n    seconds: 0,\n    milliseconds: 0,\n};\n\n// Section: Date Utilities\n\nclass DateUtils {\n    static getContext(settings) {\n        let timestamp = new Date().valueOf();\n        if (settings) {\n            timestamp -= (parseFloat(settings.today_offset_hours) || 0) * MS_IN_HOURS;\n        }\n        const todayDate = set(new Date(timestamp), timeValues);\n        return {\n            todayDate,\n            todayLabel: DateUtils.getLabel(todayDate),\n        };\n    }\n\n    static getDate(label) {\n        return set(parse(label, LABEL_FORMAT, new Date()), timeValues);\n    }\n\n    static getLabel(date) {\n        return format(date, LABEL_FORMAT);\n    }\n\n    static maybeSubstitute(todayDate, path, name) {\n        if (typeof path[name] !== 'string') {\n            // do nothing\n        } else if (path[name] === '{yesterday}') {\n            path[name] = DateUtils.getLabel(subDays(todayDate, 1));\n        } else if (path[name] === '{today}') {\n            path[name] = DateUtils.getLabel(todayDate);\n        } else if (path[name] === '{tomorrow}') {\n            path[name] = DateUtils.getLabel(addDays(todayDate, 1));\n        } else if (!isValid(DateUtils.getDate(path[name]))) {\n            assert(false, path[name]);\n        }\n    }\n}\n\nDateUtils.MonthsOfTheYear = MonthsOfTheYear;\nDateUtils.DaysOfTheWeek = DaysOfTheWeek;\n\nexport default DateUtils;\n"
  },
  {
    "path": "src/common/RichTextUtils.js",
    "content": "import assert from 'assert';\nimport deepEqual from 'deep-equal';\nimport {\n    convertFromRaw, convertToRaw,\n    EditorState, Modifier, SelectionState,\n} from 'draft-js';\nimport { draftToMarkdown, markdownToDraft } from 'markdown-draft-js';\n\nconst StorageType = {\n    MARKDOWN: 'markdown:',\n    DRAFTJS: 'draftjs:',\n};\n\nfunction toString(value) {\n    if (typeof value === 'undefined') {\n        return 'undefined';\n    }\n    return JSON.stringify(value, null, 4);\n}\n\nconst DRAFTJS_MENTION_PLUGIN_NAME = 'mention';\nconst DRAFTJS_MENTION_ENTITY_TYPE = 'mention';\nconst MARKDOWN_MENTION_PREFIX = 'mention';\n\nconst LINK_ENTITY_TYPE = 'LINK';\n\nconst draftToMarkdownOptions = {\n    entityItems: {\n        mention: {\n            open(entity) {\n                return '[';\n            },\n            close(entity) {\n                return `](${MARKDOWN_MENTION_PREFIX}:${entity.data.mention.__type__}:${entity.data.mention.__id__})`;\n            },\n        },\n    },\n};\n\nfunction postProcessDraftRawContent(rawContent) {\n    Object.values(rawContent.entityMap).forEach((entity) => {\n        if (entity.type === 'LINK') {\n            delete entity.data.href;\n            if (entity.data.url.startsWith(MARKDOWN_MENTION_PREFIX)) {\n                const parts = entity.data.url.split(':');\n                entity.type = 'mention';\n                entity.mutability = 'SEGMENTED';\n                entity.data = {\n                    mention: { __type__: parts[1], __id__: parseInt(parts[2], 10) },\n                };\n            }\n        }\n    });\n}\n\nclass RichTextUtils {\n    // eslint-disable-next-line consistent-return\n    static extractPlainText(value) {\n        if (!value) {\n            return '';\n        }\n        const content = convertFromRaw(value);\n        const blocks = content.getBlocksAsArray();\n        assert(blocks.length === 1, 'expected single line');\n        return blocks[0].getText();\n    }\n\n    static equals(left, right) {\n        if (left === right) return true;\n        if (left === null || right === null) return false;\n        const replaceKey = (block) => { delete block.key; delete block.data; };\n        left.blocks.forEach(replaceKey);\n        right.blocks.forEach(replaceKey);\n        return deepEqual(left, right);\n    }\n\n    // eslint-disable-next-line consistent-return\n    static deserialize(value, type) {\n        if (!value) {\n            if (type === StorageType.MARKDOWN) {\n                return '';\n            } if (type === StorageType.DRAFTJS) {\n                return null;\n            }\n        } else if (value.startsWith(StorageType.MARKDOWN)) {\n            const payload = value.substring(StorageType.MARKDOWN.length);\n            if (type === StorageType.MARKDOWN) {\n                return payload;\n            } if (type === StorageType.DRAFTJS) {\n                const rawContent = markdownToDraft(value);\n                postProcessDraftRawContent(rawContent);\n                return rawContent;\n            }\n        } else if (value.startsWith(StorageType.DRAFTJS)) {\n            const payload = value.substring(StorageType.DRAFTJS.length);\n            if (type === StorageType.MARKDOWN) {\n                return draftToMarkdown(JSON.parse(payload), draftToMarkdownOptions);\n            } if (type === StorageType.DRAFTJS) {\n                return JSON.parse(payload);\n            }\n        }\n        assert(false, `Invalid deserialize type: ${toString(type)} for ${toString(value)}`);\n    }\n\n    // eslint-disable-next-line consistent-return\n    static serialize(value, type) {\n        if (!value) {\n            return '';\n        } if (type === StorageType.MARKDOWN) {\n            if (typeof value === 'object') {\n                value = draftToMarkdown(value, draftToMarkdownOptions);\n            }\n            return StorageType.MARKDOWN + value;\n        } if (type === StorageType.DRAFTJS) {\n            if (typeof value === 'string') {\n                value = markdownToDraft(value);\n                postProcessDraftRawContent(value);\n            }\n            if (value) {\n                Object.values(value.entityMap).forEach((entity) => {\n                    if (entity.type === DRAFTJS_MENTION_ENTITY_TYPE) {\n                        // Do not save unnecessary fields.\n                        const original = entity.data[DRAFTJS_MENTION_PLUGIN_NAME];\n                        entity.data[DRAFTJS_MENTION_PLUGIN_NAME] = {\n                            __type__: original.__type__,\n                            __id__: original.__id__,\n                            name: original.name,\n                        };\n                    }\n                });\n                return StorageType.DRAFTJS + JSON.stringify(value);\n            }\n            return '';\n        }\n        assert(false, `Invalid serialize type: ${toString(type)}`);\n    }\n\n    static fromEditorState(editorState) {\n        const content = editorState.getCurrentContent();\n        return content.hasText() ? convertToRaw(content) : null;\n    }\n\n    static toEditorState(value) {\n        const editorState = value\n            ? EditorState.createWithContent(convertFromRaw(value))\n            : EditorState.createEmpty();\n        return EditorState.moveSelectionToEnd(editorState);\n    }\n\n    static getSelectionData(editorState) {\n        const selectionState = editorState.getSelection();\n        const blocks = editorState.getCurrentContent().getBlocksAsArray();\n        let anchorIndex = null; let\n            focusIndex = null;\n        blocks.forEach((block, index) => {\n            if (block.getKey() === selectionState.getAnchorKey()) {\n                anchorIndex = index;\n            }\n            if (block.getKey() === selectionState.getFocusKey()) {\n                focusIndex = index;\n            }\n        });\n        return {\n            anchorIndex,\n            anchorOffset: selectionState.getAnchorOffset(),\n            focusIndex,\n            focusOffset: selectionState.getFocusOffset(),\n            hasFocus: selectionState.hasFocus,\n        };\n    }\n\n    static setSelectionData(editorState, data) {\n        const blocks = editorState.getCurrentContent().getBlocksAsArray();\n        const anchorKey = blocks[data.anchorIndex].getKey();\n        const focusKey = blocks[data.focusIndex].getKey();\n        let selectionState = SelectionState.createEmpty();\n        selectionState = selectionState.merge({\n            anchorKey,\n            anchorOffset: data.anchorOffset,\n            focusKey,\n            focusOffset: data.focusOffset,\n            hasFocus: data.hasFocus,\n        });\n        return EditorState.acceptSelection(editorState, selectionState);\n    }\n\n    static fixCursorBug(prevEditorState, nextEditorState) {\n        // https://github.com/facebook/draft-js/issues/1198\n        const prevSelection = prevEditorState.getSelection();\n        const nextSelection = nextEditorState.getSelection();\n        if (\n            prevSelection.getAnchorKey() === nextSelection.getAnchorKey()\n            && prevSelection.getAnchorOffset() === 0\n            && nextSelection.getAnchorOffset() === 1\n            && prevSelection.getFocusKey() === nextSelection.getFocusKey()\n            && prevSelection.getFocusOffset() === 0\n            && nextSelection.getFocusOffset() === 1\n            && prevSelection.getHasFocus() === false\n            && nextSelection.getHasFocus() === false\n        ) {\n            const fixedSelection = nextSelection.merge({ hasFocus: true });\n            return EditorState.acceptSelection(nextEditorState, fixedSelection);\n        }\n        return nextEditorState;\n    }\n\n    static convertPlainTextToDraftContent(value, symbolToItems) {\n        if (!value) {\n            return value || '';\n        }\n        let markdown = '';\n        for (let ii = 0; ii < value.length; ii += 1) {\n            if (value[ii] in symbolToItems) {\n                const symbol = value[ii];\n                ii += 1;\n                const index = parseInt(value[ii], 10);\n                const item = symbolToItems[symbol][index];\n                markdown += `[${item.name}](mention:${item.__type__}:${item.__id__})`;\n            } else {\n                markdown += value[ii];\n            }\n        }\n        const content = markdownToDraft(markdown);\n        postProcessDraftRawContent(content);\n        return content;\n    }\n\n    static convertDraftContentToPlainText(value, symbolToItems) {\n        const markdown = RichTextUtils.deserialize(\n            value,\n            StorageType.MARKDOWN,\n        );\n\n        const mapping = {};\n        Object.entries(symbolToItems).forEach(([symbol, items]) => {\n            items.forEach((item, index) => {\n                if (item) {\n                    const key = `${item.__type__}:${item.__id__}`;\n                    if (!(key in mapping)) {\n                        mapping[key] = symbol + index;\n                    }\n                }\n            });\n        });\n\n        const regex1 = new RegExp(`(?:\\\\[.*?\\\\]\\\\(${MARKDOWN_MENTION_PREFIX}:.*?\\\\)|[^\\\\[]*)`, 'g');\n        const regex2 = new RegExp(`^\\\\[(.*?)\\\\]\\\\(${MARKDOWN_MENTION_PREFIX}:(.*?)\\\\)$`);\n        return Array.from(markdown.matchAll(regex1))\n            .map(([part]) => {\n                const result = part.match(regex2);\n                if (result) {\n                    return mapping[result[2]];\n                }\n                return part;\n            })\n            .join('');\n    }\n\n    static extractMentions(content, type) {\n        // There's no way to extract the list of entity-keys from the contentState API.\n        // And so I'm just accessing the raw data here.\n        const result = {};\n        if (!content) {\n            return result;\n        }\n        Object.values(content.entityMap)\n            .filter((entity) => entity.type === DRAFTJS_MENTION_ENTITY_TYPE)\n            .forEach((entity) => {\n                const item = entity.data[DRAFTJS_MENTION_PLUGIN_NAME];\n                if (item.__type__ === type) {\n                    result[item.__id__] = item;\n                }\n            });\n        return result;\n    }\n\n    static updateDraftContent(content, oldItems, newItems, evaluateExpressions = false) {\n        if (!content) {\n            return content;\n        }\n        if (!newItems) {\n            newItems = oldItems;\n        }\n        let contentState = convertFromRaw(content);\n\n        const keyToIndex = {};\n        oldItems.forEach((oldItem, index) => {\n            const key = `${oldItem.__type__}:${oldItem.__id__}`;\n            keyToIndex[key] = index;\n        });\n\n        const pendingEntities = [];\n\n        contentState.getBlocksAsArray().forEach((contentBlock) => {\n            const currentBlockKey = contentBlock.getKey();\n            let currentEntityKey;\n            let currentEntity;\n            contentBlock.findEntityRanges((charMetadata) => {\n                currentEntityKey = charMetadata.getEntity();\n                if (currentEntityKey) {\n                    currentEntity = contentState.getEntity(currentEntityKey);\n                    return currentEntity.getType() === DRAFTJS_MENTION_ENTITY_TYPE;\n                }\n                return false;\n            }, (start, end) => {\n                const prevItem = currentEntity.getData()[DRAFTJS_MENTION_PLUGIN_NAME];\n                const key = `${prevItem.__type__}:${prevItem.__id__}`;\n                if (key in keyToIndex) {\n                    let nextItem = newItems[keyToIndex[key]];\n                    if (typeof nextItem === 'object') {\n                        if (\n                            nextItem.__type__\n                            && prevItem.__id__ === nextItem.__id__\n                            && prevItem.name === nextItem.name\n                        ) {\n                            return; // no change\n                        }\n                        if (Array.isArray(nextItem)) { // String List\n                            nextItem = JSON.stringify(nextItem);\n                        } else {\n                            // The symbol is forwarded only for testing!\n                            nextItem = { ...nextItem, symbol: prevItem.symbol };\n                        }\n                    }\n                    pendingEntities.push([currentBlockKey, start, end, currentEntityKey, nextItem]);\n                }\n            });\n        });\n\n        pendingEntities.reverse().forEach(([blockKey, start, end, entityKey, item]) => {\n            let selectionState = SelectionState.createEmpty(blockKey);\n            selectionState = selectionState.merge({\n                anchorOffset: start,\n                focusOffset: end,\n                hasFocus: true,\n            });\n            if (typeof item === 'object') {\n                if (item.__type__) { // Item\n                    contentState = Modifier.replaceText(\n                        contentState,\n                        selectionState,\n                        item.name,\n                        null,\n                        entityKey,\n                    ).replaceEntityData(\n                        entityKey,\n                        { [DRAFTJS_MENTION_PLUGIN_NAME]: item },\n                    );\n                } else { // Rich Text\n                    const innerContentState = convertFromRaw(item);\n                    const innerContentBlocks = innerContentState.getBlocksAsArray();\n                    assert(innerContentBlocks.length === 1, 'expected single line');\n                    const innerContentBlock = innerContentBlocks[0];\n                    contentState = Modifier.replaceText(\n                        contentState,\n                        selectionState,\n                        innerContentBlock.getText(),\n                        null,\n                        null,\n                    );\n                    let currentEntityKey;\n                    innerContentBlock.findEntityRanges((charMetadata) => {\n                        currentEntityKey = charMetadata.getEntity();\n                        return !!currentEntityKey;\n                    }, (innerStart, innerEnd) => {\n                        const currentEntity = innerContentState.getEntity(currentEntityKey);\n                        contentState = contentState.createEntity(\n                            currentEntity.getType(),\n                            currentEntity.getMutability(),\n                            currentEntity.getData(),\n                        );\n                        const innerSelectionState = selectionState.merge({\n                            anchorOffset: selectionState.anchorOffset + innerStart,\n                            focusOffset: selectionState.anchorOffset + innerEnd,\n                        });\n                        contentState = Modifier.applyEntity(\n                            contentState,\n                            innerSelectionState,\n                            contentState.getLastCreatedEntityKey(),\n                        );\n                    });\n                }\n            } else {\n                // item is a string\n                contentState = Modifier.replaceText(\n                    contentState,\n                    selectionState,\n                    item,\n                    null,\n                    null,\n                );\n            }\n        });\n\n        if (evaluateExpressions) {\n            contentState = RichTextUtils.evaluateDraftContentExpressions(contentState);\n        }\n        return convertToRaw(contentState);\n    }\n\n    static addPrefixToDraftContent(contentState, items) {\n        const blocks = contentState.getBlocksAsArray();\n        assert(blocks.length === 1, 'expected single line');\n        let selectionState = SelectionState.createEmpty(blocks[0].getKey());\n        selectionState = selectionState.merge({\n            anchorOffset: 0,\n            focusOffset: 0,\n            hasFocus: true,\n        });\n        items.forEach((item) => {\n            let delta;\n            if (typeof item === 'string') {\n                contentState = Modifier.insertText(\n                    contentState,\n                    selectionState,\n                    item,\n                    null,\n                    null,\n                );\n                delta += item.length;\n            } else {\n                contentState = contentState.createEntity(\n                    DRAFTJS_MENTION_ENTITY_TYPE,\n                    'SEGMENTED',\n                    { [DRAFTJS_MENTION_PLUGIN_NAME]: item },\n                );\n                contentState = Modifier.insertText(\n                    contentState,\n                    selectionState,\n                    item.name,\n                    null,\n                    contentState.getLastCreatedEntityKey(),\n                );\n                delta += item.name.length;\n            }\n            selectionState = selectionState.merge({\n                anchorOffset: selectionState.anchorOffset + delta,\n                focusOffset: selectionState.focusOffset + delta,\n            });\n        });\n        return contentState;\n    }\n\n    static removePrefixFromDraftContext(content, prefix) {\n        let contentState = convertFromRaw(content);\n        const blocks = contentState.getBlocksAsArray();\n        assert(blocks.length === 1, 'expected single line');\n        let selectionState = SelectionState.createEmpty(blocks[0].getKey());\n        selectionState = selectionState.merge({\n            anchorOffset: 0,\n            focusOffset: prefix.length,\n            hasFocus: true,\n        });\n        // https://draftjs.org/docs/api-reference-modifier/#removerange\n        contentState = Modifier.removeRange(\n            contentState,\n            selectionState,\n            'forward',\n        );\n        return convertToRaw(contentState);\n    }\n\n    static evaluateDraftContentExpressions(contentState) {\n        const pendingUpdates = [];\n\n        contentState.getBlocksAsArray().forEach((contentBlock) => {\n            const currentBlockKey = contentBlock.getKey();\n            const currentBlockText = contentBlock.getText();\n            for (let startIndex = 0, endIndex = -1; startIndex < currentBlockText.length;) {\n                const originalStartIndex = startIndex;\n                if (currentBlockText[startIndex] === '{') {\n                    endIndex = currentBlockText.indexOf('}', startIndex);\n                    assert(endIndex !== -1, 'expected to find } after {');\n                    const expression = currentBlockText.substring(startIndex + 1, endIndex);\n                    startIndex = endIndex + 1;\n                    try {\n                        // eslint-disable-next-line no-eval\n                        const result = eval(expression).toString();\n                        pendingUpdates.push({\n                            blockKey: currentBlockKey,\n                            startIndex: originalStartIndex,\n                            endIndex: startIndex,\n                            text: result,\n                        });\n                    } catch (error) {\n                        // eslint-disable-next-line no-console\n                        console.warn(expression, error);\n                    }\n                } else if (currentBlockText[startIndex] === '[') {\n                    try {\n                        endIndex = currentBlockText.indexOf(']', startIndex);\n                        assert(endIndex !== -1, 'expected to find ] after [');\n                        const linkText = currentBlockText.substring(startIndex + 1, endIndex);\n                        startIndex = endIndex + 1;\n                        assert(currentBlockText[startIndex] === '(', 'expected to find ( after ]');\n                        endIndex = currentBlockText.indexOf(')', startIndex);\n                        assert(endIndex !== -1, 'expected to find ) after (');\n                        const linkHref = currentBlockText.substring(startIndex + 1, endIndex);\n                        startIndex = endIndex + 1;\n\n                        contentState = contentState.createEntity(\n                            LINK_ENTITY_TYPE,\n                            'IMMUTABLE',\n                            { url: linkHref },\n                        );\n                        pendingUpdates.push({\n                            blockKey: currentBlockKey,\n                            startIndex: originalStartIndex,\n                            endIndex: startIndex,\n                            text: linkText,\n                            entityKey: contentState.getLastCreatedEntityKey(),\n                        });\n                    } catch (error) {\n                        // eslint-disable-next-line no-console\n                        console.warn(error);\n                    }\n                } else {\n                    startIndex += 1;\n                }\n            }\n        });\n\n        pendingUpdates.reverse().forEach(({\n            blockKey, startIndex, endIndex, text, entityKey,\n        }) => {\n            let selectionState = SelectionState.createEmpty(blockKey);\n            selectionState = selectionState.merge({\n                anchorOffset: startIndex,\n                focusOffset: endIndex,\n                hasFocus: true,\n            });\n            contentState = Modifier.replaceText(\n                contentState,\n                selectionState,\n                text,\n                null,\n                entityKey || null,\n            );\n        });\n\n        return contentState;\n    }\n}\n\nRichTextUtils.StorageType = StorageType;\n\nexport default RichTextUtils;\n"
  },
  {
    "path": "src/common/SocketRPC.js",
    "content": "import assert from 'assert';\n\nconst SERVER_SIDE = 'server_side';\nconst CLIENT_SIDE = 'client_side';\n\nconst GENERAL_REQUEST = 'general-request-';\nconst GENERAL_RESPONSE = 'general-response-';\nconst GENERAL_SUBSCRIPTION = 'general-subscription';\nconst LOG_SUBSCRIPTION = 'log-subscription';\n\nfunction _remove(list, value) {\n    const index = list.indexOf(value);\n    if (index !== -1) list.splice(index, 1);\n}\n\nexport default class SocketRPC {\n    static clients = [];\n\n    static server(socket, actions) {\n        const instance = new SocketRPC(SERVER_SIDE, socket);\n        actions.registerBroadcast(instance);\n        instance.registerActions(actions);\n        return instance;\n    }\n\n    static client(socket, thenCallback, catchCallback) {\n        const instance = new SocketRPC(CLIENT_SIDE, socket, thenCallback, catchCallback);\n        instance.registerSubscriptions();\n        return instance;\n    }\n\n    constructor(type, socket, thenCallback, catchCallback) {\n        this.type = type;\n        this.socket = socket;\n        if (type === SERVER_SIDE) {\n            SocketRPC.clients.push(this);\n            this.socket.on('disconnect', () => _remove(SocketRPC.clients, this));\n        } else if (type === CLIENT_SIDE) {\n            this.counter = 0;\n            this.subscriptions = {};\n            this.thenCallback = thenCallback;\n            this.catchCallback = catchCallback;\n        }\n    }\n\n    // Normal RPC\n\n    send(name, request) {\n        assert(this.type === CLIENT_SIDE);\n        const promise = new Promise((resolve, reject) => {\n            this.counter += 1;\n            const { counter } = this;\n            const responseEventName = GENERAL_RESPONSE + counter.toString();\n            this.socket.once(responseEventName, (wrapper) => {\n                const { response, error } = wrapper;\n                if (!error) {\n                    resolve(response);\n                } else {\n                    reject(error);\n                }\n            });\n            this.socket.emit(GENERAL_REQUEST, { counter, name, request });\n        });\n        return promise\n            .then((data) => {\n                this.thenCallback(name, request, data);\n                return data;\n            })\n            .catch((error) => {\n                this.catchCallback(name, request, error);\n                throw error; // this can be ignored\n            });\n    }\n\n    registerActions(actions) {\n        assert(this.type === SERVER_SIDE);\n        this.socket.on(GENERAL_REQUEST, async (wrapper) => {\n            const { counter, name, request } = wrapper;\n            const complete = false;\n            const responseEventName = GENERAL_RESPONSE + counter.toString();\n            const resolve = (response) => {\n                assert(!complete, 'already completed');\n                this.socket.emit(responseEventName, { counter, response });\n            };\n            const reject = (error) => {\n                assert(!complete, 'already completed');\n                error = `${name}: ${JSON.stringify(request, null, 4)}\\n\\n${error}`;\n                this.socket.emit(responseEventName, { counter, error });\n            };\n            if (!actions.has(name)) {\n                reject(`Unknown action: ${name}`);\n                return;\n            }\n            try {\n                const result = await actions.invoke(name, request);\n                resolve(result || null);\n            } catch (error) {\n                reject(error.stack.toString());\n            }\n        });\n    }\n\n    // Subscriptions\n\n    registerSubscriptions() {\n        assert(this.type === CLIENT_SIDE);\n        this.socket.on(GENERAL_SUBSCRIPTION, async (wrapper) => {\n            const { name, data } = wrapper;\n            const futures = this.subscriptions[name];\n            if (futures) {\n                futures.forEach(({ resolve }) => resolve(data));\n            }\n            delete this.subscriptions[name];\n        });\n        this.socket.on(LOG_SUBSCRIPTION, async (wrapper) => {\n            const { args } = wrapper;\n            try {\n                const [level, ...moreArgs] = args;\n                // eslint-disable-next-line no-console\n                console[level](...moreArgs);\n            } catch {\n                // eslint-disable-next-line no-console\n                console.error(...args);\n            }\n        });\n    }\n\n    subscribe(name) {\n        assert(this.type === CLIENT_SIDE);\n        if (!(name in this.subscriptions)) {\n            this.subscriptions[name] = [];\n        }\n        let future;\n        const promise = new Promise((resolve, reject) => {\n            future = { resolve, reject };\n            this.subscriptions[name].push(future);\n        });\n        const cancel = () => _remove(this.subscriptions[name], future);\n        return { promise, cancel };\n    }\n\n    broadcast(name, data) {\n        assert(this.type === SERVER_SIDE);\n        SocketRPC.clients.forEach(\n            (client) => client.socket.emit(GENERAL_SUBSCRIPTION, { name, data }),\n        );\n    }\n\n    /**\n     * This is separate from the broadcast method because those are buffered\n     * until the transaction is successfully completed.\n     */\n    log(...args) {\n        assert(this.type === SERVER_SIDE);\n        SocketRPC.clients.forEach((client) => client.socket.emit(LOG_SUBSCRIPTION, { args }));\n    }\n}\n"
  },
  {
    "path": "src/common/__tests__/RichTextUtils.test.js",
    "content": "import RichTextUtils from '../RichTextUtils';\n\nconst { StorageType } = RichTextUtils;\n\nconst typeToValue = {\n    [StorageType.MARKDOWN]: 'markdown:Normal [Kirtivardhan Rathore](mention:log-topic:3) [Link](facebook.com) Text\\n\\n# Heading 1\\n\\n#### Heading 4\\n\\n```\\nCode\\n```\\n\\n> Quote\\n\\n- List Item 1\\n- List Item 2',\n    [StorageType.DRAFTJS]: 'draftjs:{\"blocks\":[{\"key\":\"3f9if\",\"text\":\"Normal Kirtivardhan Rathore Link Text\",\"type\":\"unstyled\",\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[{\"offset\":7,\"length\":20,\"key\":0},{\"offset\":28,\"length\":4,\"key\":1}],\"data\":{}},{\"key\":\"c8ie\",\"text\":\"Heading 1\",\"type\":\"header-one\",\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}},{\"key\":\"eq8v6\",\"text\":\"Heading 4\",\"type\":\"header-four\",\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}},{\"key\":\"b674h\",\"text\":\"Code\",\"type\":\"code-block\",\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}},{\"key\":\"12dll\",\"text\":\"Quote\",\"type\":\"blockquote\",\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}},{\"key\":\"288di\",\"text\":\"List Item 1\",\"type\":\"unordered-list-item\",\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}},{\"key\":\"3hmqp\",\"text\":\"List Item 2\",\"type\":\"unordered-list-item\",\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{\"0\":{\"type\":\"mention\",\"mutability\":\"SEGMENTED\",\"data\":{\"mention\":{\"__type__\":\"log-topic\",\"__id__\":3}}},\"1\":{\"type\":\"LINK\",\"mutability\":\"MUTABLE\",\"data\":{\"url\":\"facebook.com\"}}}}',\n};\n\nfunction verify(inputType, outputType) {\n    const expectedValue = typeToValue[outputType];\n    const actualValue = RichTextUtils.serialize(\n        RichTextUtils.deserialize(typeToValue[inputType], inputType),\n        outputType,\n    );\n    if (outputType === StorageType.DRAFTJS) {\n        const value1 = RichTextUtils.deserialize(expectedValue, StorageType.DRAFTJS);\n        const value2 = RichTextUtils.deserialize(actualValue, StorageType.DRAFTJS);\n        expect(RichTextUtils.equals(value1, value2)).toBeTruthy();\n    } else {\n        expect(actualValue).toEqual(expectedValue);\n    }\n}\n\ntest('test_storage_type_conversion', () => {\n    verify(StorageType.MARKDOWN, StorageType.MARKDOWN);\n    verify(StorageType.MARKDOWN, StorageType.DRAFTJS);\n    verify(StorageType.DRAFTJS, StorageType.MARKDOWN);\n    verify(StorageType.DRAFTJS, StorageType.DRAFTJS);\n});\n"
  },
  {
    "path": "src/common/data_types/LogEvent.js",
    "content": "import assert from 'assert';\n\nimport RichTextUtils from '../RichTextUtils';\nimport DataTypeBase from './base';\nimport LogKey from './LogKey';\nimport LogStructure from './LogStructure';\nimport { getVirtualID } from './utils';\nimport { validateRecursive } from './validation';\n\nconst { LogLevel } = LogStructure;\n\nclass LogEvent extends DataTypeBase {\n    static createVirtual({\n        date = null,\n        title = null,\n        details = null,\n        logLevel = LogLevel.getIndex(LogLevel.NORMAL),\n        logStructure = null,\n        isFavorite = false,\n        isComplete = true,\n    }) {\n        if (typeof date !== 'string' || date.match(/\\(/)) {\n            // In case the filters are gt(null) or lt(null) or dateRange, default to null.\n            date = null;\n        }\n        // Abstraction leak! The LogEventSearch component filters to logLevels = [2,3] by default.\n        if (Array.isArray(logLevel)) {\n            [logLevel] = logLevel;\n        }\n        const logEvent = {\n            __type__: 'log-event',\n            date,\n            orderingIndex: null,\n            __id__: getVirtualID(),\n            title,\n            details,\n            logLevel,\n            logStructure,\n            isFavorite,\n            isComplete,\n        };\n        LogEvent.addDefaultStructureValues(logEvent);\n        LogEvent.trigger(logEvent);\n        return logEvent;\n    }\n\n    static addDefaultStructureValues(logEvent) {\n        if (logEvent.logStructure) {\n            logEvent.logStructure.eventKeys = logEvent.logStructure.eventKeys.map((logKey) => ({\n                ...logKey,\n                value: logKey.value\n                    || LogKey.Type[logKey.type].getDefault(logKey)\n                    || null,\n            }));\n        }\n    }\n\n    static async updateWhere(where) {\n        if (where.date && typeof where.date === 'object') {\n            where.date = {\n                [this.database.Op.gte]: where.date.startDate,\n                [this.database.Op.lte]: where.date.endDate,\n            };\n        } else if (typeof where.date === 'string') {\n            const reResult = where.date.match(/^(\\w+)\\(([\\w-]+)\\)$/);\n            if (reResult) {\n                const operator = this.database.Op[reResult[1]];\n                const value = reResult[1] === 'null' ? null : reResult[2];\n                where.date = { [operator]: value };\n            }\n        }\n        if (where.title) {\n            where.title = {\n                [this.database.Op.like]: `%${where.title}%`,\n            };\n        }\n        if (where.details) {\n            where.details = {\n                [this.database.Op.like]: `%${where.details}%`,\n            };\n        }\n        await DataTypeBase.updateWhere.call(this, where, {\n            date: 'date',\n            title: 'title',\n            details: 'details',\n            logStructure: 'structure_id',\n            isFavorite: 'is_favorite',\n            isComplete: 'is_complete',\n            logLevel: 'log_level',\n        });\n    }\n\n    static trigger(logEvent) {\n        if (logEvent.logStructure) {\n            const getLogKeyValue = (logKey) => logKey.value || (logKey.isOptional ? '' : logKey);\n            logEvent.logStructure.eventKeys.forEach((logKey, index) => {\n                if (!logKey.template) {\n                    return;\n                }\n                const previousEventKeys = logEvent.logStructure.eventKeys.slice(0, index);\n                logKey.value = RichTextUtils.extractPlainText(\n                    RichTextUtils.updateDraftContent(\n                        logKey.template,\n                        previousEventKeys,\n                        previousEventKeys.map(getLogKeyValue),\n                        true, // evaluateExpressions\n                    ),\n                );\n            });\n            logEvent.title = RichTextUtils.updateDraftContent(\n                logEvent.logStructure.eventTitleTemplate,\n                logEvent.logStructure.eventKeys,\n                logEvent.logStructure.eventKeys.map((logKey) => logKey.value || (logKey.isOptional ? '' : logKey)),\n                true, // evaluateExpressions\n            );\n            logEvent.logLevel = logEvent.logStructure.logLevel;\n        }\n    }\n\n    static async updateLogTopicsInTitleAndDetails(inputLogEvent) {\n        const originalLogTopics = Object.values({\n            ...RichTextUtils.extractMentions(inputLogEvent.title, 'log-topic'),\n            ...RichTextUtils.extractMentions(inputLogEvent.details, 'log-topic'),\n        });\n        const updatedLogTopics = await Promise.all(\n            originalLogTopics.map((originalTopic) => this.invoke.call(\n                this,\n                'log-topic-load-partial',\n                originalTopic,\n            )),\n        );\n        inputLogEvent.title = RichTextUtils.updateDraftContent(\n            inputLogEvent.title,\n            originalLogTopics,\n            updatedLogTopics,\n        );\n        inputLogEvent.details = RichTextUtils.updateDraftContent(\n            inputLogEvent.details,\n            originalLogTopics,\n            updatedLogTopics,\n        );\n        return updatedLogTopics.map((logTopic) => logTopic.__id__);\n    }\n\n    static async updateLogTopics(inputLogEvent) {\n        const promises = [];\n        promises.push(LogEvent.updateLogTopicsInTitleAndDetails.call(this, inputLogEvent));\n        if (inputLogEvent.logStructure) {\n            inputLogEvent.logStructure.eventKeys.forEach((inputLogKey) => {\n                promises.push(LogKey.updateLogTopics.call(this, inputLogKey));\n            });\n        }\n        const listOfTopicIDs = await Promise.all(promises);\n        return listOfTopicIDs.flat();\n    }\n\n    static async validate(inputLogEvent) {\n        const results = [];\n\n        results.push([\n            '.title',\n            !!inputLogEvent.title,\n            'must be non-empty.',\n        ]);\n\n        if (inputLogEvent.logStructure) {\n            const logStructureResults = await validateRecursive.call(\n                this,\n                LogStructure,\n                '.logStructure',\n                inputLogEvent.logStructure,\n            );\n            results.push(...logStructureResults);\n\n            results.push([\n                '.logStructure.eventAllowDetails',\n                inputLogEvent.logStructure.eventAllowDetails\n                    ? true\n                    : inputLogEvent.details === null,\n                'does not allow .details',\n            ]);\n\n            const logKeyResults = await Promise.all(\n                inputLogEvent.logStructure.eventKeys.map(\n                    async (inputLogKey, index) => LogKey.validateValue.call(\n                        this,\n                        inputLogKey,\n                        index,\n                    ),\n                ),\n            );\n            results.push(...logKeyResults.filter((result) => result));\n        }\n\n        if (inputLogEvent.isComplete) {\n            results.push([\n                '.date',\n                inputLogEvent.date !== null,\n                'should not be null.',\n            ]);\n        }\n        return results;\n    }\n\n    static async load(id) {\n        const logEvent = await this.database.findByPk('LogEvent', id);\n        let outputLogStructure = null;\n        if (logEvent.structure_id) {\n            outputLogStructure = await LogStructure.load.call(this, logEvent.structure_id);\n            const structureValues = JSON.parse(logEvent.structure_values);\n            outputLogStructure.eventKeys.forEach((logKey, index) => {\n                logKey.value = structureValues[index] || null;\n            });\n        } else {\n            assert(logEvent.structure_values === null);\n        }\n        return {\n            __type__: 'log-event',\n            __id__: logEvent.id,\n            date: logEvent.date,\n            isComplete: logEvent.is_complete,\n            orderingIndex: logEvent.ordering_index,\n            title: RichTextUtils.deserialize(\n                logEvent.title,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            details: RichTextUtils.deserialize(\n                logEvent.details,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            logLevel: logEvent.log_level,\n            isFavorite: logEvent.is_favorite,\n            logStructure: outputLogStructure,\n        };\n    }\n\n    static async save(inputLogEvent) {\n        let logEvent = await this.database.findItem('LogEvent', inputLogEvent);\n\n        DataTypeBase.broadcast.call(this, 'log-event-list', logEvent, { date: inputLogEvent.date });\n\n        // Before the serialization process, since the input is modified.\n        const targetLogTopicIDs = await LogEvent.updateLogTopics.call(this, inputLogEvent);\n\n        const shouldResetOrderingIndex = logEvent ? (\n            logEvent.date !== inputLogEvent.date\n            || logEvent.is_complete !== inputLogEvent.isComplete\n        ) : true;\n        const orderingIndexWhere = {\n            date: inputLogEvent.date,\n            is_complete: inputLogEvent.isComplete,\n        };\n        const orderingIndex = await DataTypeBase.getOrderingIndex\n            .call(this, shouldResetOrderingIndex ? null : logEvent, orderingIndexWhere);\n        let logValues;\n        if (inputLogEvent.logStructure) {\n            logValues = inputLogEvent.logStructure.eventKeys.map(\n                (eventKey) => eventKey.value || null,\n            );\n        }\n        const updated = {\n            date: inputLogEvent.date,\n            ordering_index: orderingIndex,\n            title: RichTextUtils.serialize(\n                inputLogEvent.title,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            details: RichTextUtils.serialize(\n                inputLogEvent.details,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            log_level: inputLogEvent.logLevel,\n            is_favorite: inputLogEvent.isFavorite,\n            is_complete: inputLogEvent.isComplete,\n            structure_id: inputLogEvent.logStructure ? inputLogEvent.logStructure.__id__ : null,\n            structure_values: logValues ? JSON.stringify(logValues) : null,\n        };\n        logEvent = await this.database.createOrUpdateItem('LogEvent', logEvent, updated);\n\n        await this.database.setEdges(\n            'LogEventToLogTopic',\n            'source_event_id',\n            logEvent.id,\n            'target_topic_id',\n            Object.values(targetLogTopicIDs).reduce((result, topicID) => {\n                // eslint-disable-next-line no-param-reassign\n                result[topicID] = {};\n                return result;\n            }, {}),\n        );\n        await this.invoke.call(\n            this,\n            'structure-value-typeahead-index-$refresh',\n            { structure_id: logEvent.structure_id },\n        );\n\n        this.broadcast('reminder-sidebar');\n        return logEvent.id;\n    }\n\n    static async delete(id) {\n        const logEvent = await this.database.deleteByPk('LogEvent', id);\n        DataTypeBase.broadcast.call(this, 'log-event-list', logEvent, ['date']);\n        await this.invoke.call(\n            this,\n            'structure-value-typeahead-index-$refresh',\n            { structure_id: logEvent.structure_id },\n        );\n        return { __id__: logEvent.id };\n    }\n}\n\nLogEvent.LogLevel = LogLevel;\n\nexport default LogEvent;\n"
  },
  {
    "path": "src/common/data_types/LogKey.js",
    "content": "import RichTextUtils from '../RichTextUtils';\nimport Enum from './enum';\nimport { getPartialItem, getVirtualID } from './utils';\nimport { validateNonEmptyString } from './validation';\n\nconst LogKeyType = Enum([\n    {\n        value: 'string',\n        label: 'String',\n        validator: async () => true,\n        getDefault: () => '',\n    },\n    {\n        value: 'string_list',\n        label: 'String List',\n        validator: async (value) => Array.isArray(value),\n        getDefault: () => [],\n    },\n    {\n        value: 'integer',\n        label: 'Integer',\n        validator: async (value) => !!value.match(/^\\d+$/),\n        getDefault: () => '',\n    },\n    {\n        value: 'number',\n        label: 'Number',\n        validator: async (value) => !!value.match(/^\\d+(?:\\.\\d+)?$/),\n        getDefault: () => '',\n    },\n    {\n        value: 'time',\n        label: 'Time',\n        validator: async (value) => !!value.match(/^\\d{2}:\\d{2}$/),\n        getDefault: () => '',\n    },\n    {\n        value: 'yes_or_no',\n        label: 'Yes / No',\n        validator: async (value) => !!value.match(/^(?:yes|no)$/),\n        getDefault: () => 'no',\n    },\n    {\n        value: 'enum',\n        label: 'Enum',\n        validator: async (value, logKey) => logKey.enumValues.includes(value),\n        getDefault: (logKey) => logKey.enumValues[0],\n    },\n    {\n        value: 'log_topic',\n        label: 'Topic',\n        validator: async (value, logKey, that) => {\n            const logTopic = await that.invoke.call(that, 'log-topic-load', value);\n            return logTopic.parentLogTopic.__id__ === logKey.parentLogTopic.__id__;\n        },\n        getDefault: () => null,\n    },\n    {\n        value: 'rich_text_line',\n        label: 'Rich Text Line',\n        validator: async (value) => true,\n        getDefault: () => null,\n    },\n    {\n        value: 'link',\n        label: 'Link',\n        validator: async (value) => true,\n        getDefault: () => '',\n    },\n]);\n\nclass LogKey {\n    static createVirtual() {\n        return {\n            __type__: 'log-structure-key',\n            __id__: getVirtualID(),\n            name: '',\n            type: LogKeyType.STRING,\n            isOptional: false,\n            template: null,\n            enumValues: [],\n            parentLogTopic: null,\n        };\n    }\n\n    static async validate(inputLogKey) {\n        const results = [];\n        results.push(validateNonEmptyString('.name', inputLogKey.name));\n        results.push(validateNonEmptyString('.type', inputLogKey.type));\n        if (inputLogKey.type === LogKeyType.ENUM) {\n            results.push([\n                '.enumValues',\n                inputLogKey.enumValues.length > 0,\n                'must be provided!',\n            ]);\n        } if (inputLogKey.type === LogKeyType.LOG_TOPIC) {\n            results.push([\n                '.parentLogTopic',\n                inputLogKey.parentLogTopic,\n                'must be provided!',\n            ]);\n        }\n        return results;\n    }\n\n    static async validateValue(inputLogKey, index) {\n        if (inputLogKey.isOptional && !inputLogKey.value) return null;\n        const name = `.logKeys[${index}].value`;\n        if (!inputLogKey.value) return [name, false, 'must be non-empty.'];\n        const KeyOption = LogKeyType[inputLogKey.type];\n        let isValid = await KeyOption.validator(inputLogKey.value, inputLogKey, this);\n        if (!isValid && KeyOption.maybeFix) {\n            const fixedValue = KeyOption.maybeFix(inputLogKey.value, inputLogKey);\n            if (fixedValue) {\n                inputLogKey.value = fixedValue;\n                isValid = true;\n            }\n        }\n        return [name, isValid, 'fails validation for specified type.'];\n    }\n\n    static async load(rawLogKey, index) {\n        let parentLogTopic = null;\n        if (rawLogKey.parent_topic_id) {\n            // Normally, we would use \"log-topic-load\" here, but it does a lot of extra work.\n            const logTopic = await this.database.findByPk('LogTopic', rawLogKey.parent_topic_id);\n            parentLogTopic = {\n                __type__: 'log-topic',\n                __id__: logTopic.id,\n                name: logTopic.name,\n            };\n        }\n        return {\n            __type__: 'log-structure-key',\n            __id__: index,\n            name: rawLogKey.name,\n            type: rawLogKey.type,\n            template: rawLogKey.template || null,\n            isOptional: rawLogKey.is_optional || false,\n            enumValues: rawLogKey.enum_values || [],\n            parentLogTopic,\n        };\n    }\n\n    static save(inputLogKey) {\n        const result = {\n            name: inputLogKey.name,\n            type: inputLogKey.type,\n        };\n        if (inputLogKey.isOptional) {\n            result.is_optional = true;\n        }\n        if (inputLogKey.template) {\n            result.template = inputLogKey.template;\n        }\n        if (inputLogKey.type === LogKeyType.ENUM && inputLogKey.enumValues) {\n            result.enum_values = inputLogKey.enumValues;\n        }\n        if (inputLogKey.type === LogKeyType.LOG_TOPIC && inputLogKey.parentLogTopic) {\n            result.parent_topic_id = inputLogKey.parentLogTopic.__id__;\n        }\n        return result;\n    }\n\n    static async updateLogTopicsInLogTopicType(inputLogKey) {\n        const originalLogTopics = [];\n        originalLogTopics.push(inputLogKey.parentLogTopic);\n        if (inputLogKey.value) {\n            originalLogTopics.push(inputLogKey.value);\n        }\n        const updatedLogTopics = await Promise.all(\n            originalLogTopics.map((originalLogTopic) => this.invoke.call(\n                this,\n                'log-topic-load-partial',\n                originalLogTopic,\n            )),\n        );\n        inputLogKey.parentLogTopic = getPartialItem(updatedLogTopics[0]);\n        if (inputLogKey.value) {\n            inputLogKey.value = getPartialItem(updatedLogTopics[1]);\n        }\n        return updatedLogTopics.map((logTopic) => logTopic.__id__);\n    }\n\n    static async updateLogTopicsInRichTextLineType(inputLogKey) {\n        const originalLogTopics = Object.values(RichTextUtils.extractMentions(inputLogKey.value, 'log-topic'));\n        const updatedLogTopics = await Promise.all(\n            originalLogTopics.map((originalLogTopic) => this.invoke.call(\n                this,\n                'log-topic-load-partial',\n                originalLogTopic,\n            )),\n        );\n        inputLogKey.value = RichTextUtils.updateDraftContent(\n            inputLogKey.value,\n            originalLogTopics,\n            updatedLogTopics,\n        );\n        return updatedLogTopics.map((logTopic) => logTopic.__id__);\n    }\n\n    static async updateLogTopics(inputLogKey) {\n        if (inputLogKey.type === LogKeyType.LOG_TOPIC) {\n            return LogKey.updateLogTopicsInLogTopicType.call(this, inputLogKey);\n        } if (inputLogKey.type === LogKeyType.RICH_TEXT_LINE) {\n            return LogKey.updateLogTopicsInRichTextLineType.call(this, inputLogKey);\n        }\n        return [];\n    }\n}\n\nLogKey.Type = LogKeyType;\n\nexport default LogKey;\n"
  },
  {
    "path": "src/common/data_types/LogStructure.js",
    "content": "import { asyncSequence } from '../AsyncUtils';\nimport RichTextUtils from '../RichTextUtils';\nimport DataTypeBase from './base';\nimport Enum from './enum';\nimport LogKey from './LogKey';\nimport LogStructureFrequency from './LogStructureFrequency';\nimport LogStructureGroup from './LogStructureGroup';\nimport { getPartialItem, getVirtualID, isVirtualItem } from './utils';\nimport { validateRecursive, validateRecursiveList } from './validation';\n\nconst LogLevel = Enum([\n    {\n        value: 'minor',\n        label: 'Minor (1)',\n        index: 1,\n    },\n    {\n        value: 'normal',\n        label: 'Normal (2)',\n        index: 2,\n    },\n    {\n        value: 'major',\n        label: 'Major (3)',\n        index: 3,\n    },\n]);\n\nLogLevel.getIndex = (value) => LogLevel[value].index;\nLogLevel.getValue = (index) => LogLevel.Options[index - 1].value;\n\nclass LogStructure extends DataTypeBase {\n    static createVirtual({ logStructureGroup, name = '' }) {\n        return {\n            __type__: 'log-structure',\n            __id__: getVirtualID(),\n            logStructureGroup,\n            name,\n            details: null,\n            eventAllowDetails: false,\n            eventKeys: [],\n            eventTitleTemplate: null,\n            eventNeedsEdit: false,\n            isPeriodic: false,\n            reminderText: null,\n            frequency: null,\n            frequencyArgs: null,\n            warningDays: null,\n            suppressUntilDate: null,\n            logLevel: LogLevel.getIndex(LogLevel.NORMAL),\n            isFavorite: false,\n            isDeprecated: false,\n        };\n    }\n\n    static async updateWhere(where) {\n        await DataTypeBase.updateWhere.call(this, where, {\n            __id__: 'id',\n            logStructureGroup: 'group_id',\n            name: 'name',\n            isPeriodic: 'is_periodic',\n            isFavorite: 'is_favorite',\n            isDeprecated: 'is_deprecated',\n        });\n    }\n\n    static trigger(logStructure) {\n        // TODO: If an eventKey is deleted, remove it from the content.\n        const options = [getPartialItem(logStructure), ...logStructure.eventKeys];\n        if (logStructure.name && !logStructure.eventTitleTemplate) {\n            logStructure.eventTitleTemplate = RichTextUtils.convertPlainTextToDraftContent('$0', {\n                $: [logStructure],\n            });\n        }\n        logStructure.eventTitleTemplate = RichTextUtils.updateDraftContent(\n            logStructure.eventTitleTemplate,\n            options,\n            options,\n        );\n        if (logStructure.eventKeys.length) {\n            logStructure.eventNeedsEdit = true;\n        }\n    }\n\n    static async updateLogTopicsInTitleTemplateAndDetails(inputLogStructure) {\n        const originalLogTopics = Object.values({\n            ...RichTextUtils.extractMentions(inputLogStructure.eventTitleTemplate, 'log-topic'),\n            ...RichTextUtils.extractMentions(inputLogStructure.details, 'log-topic'),\n        });\n        const updatedLogTopics = await Promise.all(\n            originalLogTopics.map((originalTopic) => this.invoke.call(\n                this,\n                'log-topic-load-partial',\n                originalTopic,\n            )),\n        );\n        inputLogStructure.eventTitleTemplate = RichTextUtils.updateDraftContent(\n            inputLogStructure.eventTitleTemplate,\n            originalLogTopics,\n            updatedLogTopics,\n        );\n        inputLogStructure.details = RichTextUtils.updateDraftContent(\n            inputLogStructure.details,\n            originalLogTopics,\n            updatedLogTopics,\n        );\n        return updatedLogTopics.map((logTopic) => logTopic.__id__);\n    }\n\n    static async updateLogTopics(inputLogStructure) {\n        const promises = [];\n        promises.push(\n            LogStructure.updateLogTopicsInTitleTemplateAndDetails.call(this, inputLogStructure),\n        );\n        inputLogStructure.eventKeys.forEach((inputLogKey) => {\n            promises.push(LogKey.updateLogTopics.call(this, inputLogKey));\n        });\n        const listOfTopicIDs = await Promise.all(promises);\n        return listOfTopicIDs.flat();\n    }\n\n    static async validate(inputLogStructure) {\n        const results = [];\n\n        if (inputLogStructure.logStructureGroup) {\n            const logStructureGroupResults = await validateRecursive.call(\n                this,\n                LogStructureGroup,\n                '.logStructureGroup',\n                inputLogStructure.logStructureGroup,\n            );\n            results.push(...logStructureGroupResults);\n        } else {\n            results.push([\n                '.logStructureGroup',\n                false,\n                'must be provided!',\n            ]);\n        }\n\n        results.push(...await validateRecursiveList.call(\n            this,\n            LogKey,\n            '.eventKeys',\n            inputLogStructure.eventKeys,\n        ));\n\n        results.push([\n            '.eventTitleTemplate',\n            inputLogStructure.__id__ in RichTextUtils.extractMentions(\n                inputLogStructure.eventTitleTemplate,\n                'log-structure',\n            ),\n            'must mention the structure!',\n        ]);\n\n        if (inputLogStructure.isPeriodic) {\n            results.push([\n                '.isPeriodic',\n                inputLogStructure.frequency !== null\n                && inputLogStructure.suppressUntilDate !== null,\n                'requires frequency & suppressUntilDate is set.',\n            ]);\n        } else {\n            results.push([\n                '.isPeriodic',\n                inputLogStructure.frequency === null\n                && inputLogStructure.suppressUntilDate === null,\n                'requires frequency & suppressUntilDate to be unset.',\n            ]);\n        }\n\n        return results;\n    }\n\n    static async load(id) {\n        const logStructure = await this.database.findByPk('LogStructure', id);\n        const outputLogStructureGroup = await this.invoke.call(\n            this,\n            'log-structure-group-load',\n            { __id__: logStructure.group_id },\n        );\n        const eventKeys = await Promise.all(\n            JSON.parse(logStructure.event_keys).map(\n                (eventKey, index) => LogKey.load.call(this, eventKey, index + 1),\n            ),\n        );\n        return {\n            __type__: 'log-structure',\n            __id__: logStructure.id,\n            logStructureGroup: outputLogStructureGroup,\n            name: logStructure.name,\n            details: RichTextUtils.deserialize(\n                logStructure.details,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            eventAllowDetails: logStructure.event_allow_details,\n            eventKeys,\n            eventTitleTemplate: RichTextUtils.deserialize(\n                logStructure.event_title_template,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            eventNeedsEdit: logStructure.event_needs_edit,\n            isPeriodic: logStructure.is_periodic,\n            reminderText: logStructure.reminder_text,\n            frequency: logStructure.frequency,\n            frequencyArgs: logStructure.frequency_args,\n            warningDays: logStructure.warning_days,\n            suppressUntilDate: logStructure.suppress_until_date,\n            logLevel: logStructure.log_level,\n            isFavorite: logStructure.is_favorite,\n            isDeprecated: logStructure.is_deprecated,\n        };\n    }\n\n    static async save(inputLogStructure) {\n        const logStructure = await this.database.findItem('LogStructure', inputLogStructure);\n        const originalLogStructure = logStructure ? { ...logStructure.dataValues } : null;\n\n        DataTypeBase.broadcast.call(\n            this,\n            'log-structure-list',\n            logStructure,\n            { group_id: inputLogStructure.logStructureGroup.__id__ },\n        );\n\n        // Before the serialization process, since the input is modified.\n        const targetLogTopicIDs = await LogStructure.updateLogTopics.call(this, inputLogStructure);\n\n        const orderingIndex = await DataTypeBase.getOrderingIndex.call(this, logStructure);\n        const updated = {\n            group_id: inputLogStructure.logStructureGroup.__id__,\n            ordering_index: orderingIndex,\n            name: inputLogStructure.name,\n            details: RichTextUtils.serialize(\n                inputLogStructure.details,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            event_allow_details: inputLogStructure.eventAllowDetails,\n            event_keys: JSON.stringify(inputLogStructure.eventKeys.map(\n                (eventKey) => LogKey.save.call(this, eventKey),\n            )),\n            event_title_template: RichTextUtils.serialize(\n                inputLogStructure.eventTitleTemplate,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            event_needs_edit: inputLogStructure.eventNeedsEdit,\n            is_periodic: inputLogStructure.isPeriodic,\n            reminder_text: inputLogStructure.reminderText,\n            frequency: inputLogStructure.frequency,\n            frequency_args: inputLogStructure.frequencyArgs,\n            warning_days: inputLogStructure.warningDays,\n            suppress_until_date: inputLogStructure.suppressUntilDate,\n            log_level: inputLogStructure.logLevel,\n            is_favorite: inputLogStructure.isFavorite,\n            is_deprecated: inputLogStructure.isDeprecated,\n        };\n\n        // Fetch affected logEvents BEFORE updating the database.\n        // Why? To prevent loading the new log-structure from the log-event.\n        let inputLogEvents = null;\n        if (originalLogStructure) {\n            let shouldRegenerateLogEvents = false;\n            if (!shouldRegenerateLogEvents) {\n                shouldRegenerateLogEvents = (\n                    originalLogStructure.name !== updated.name\n                    || originalLogStructure.event_keys !== updated.event_keys\n                );\n            }\n            if (!shouldRegenerateLogEvents) {\n                const originalTitleTemplate = RichTextUtils.deserialize(\n                    originalLogStructure.event_title_template,\n                    RichTextUtils.StorageType.DRAFTJS,\n                );\n                shouldRegenerateLogEvents = !RichTextUtils.equals(\n                    originalTitleTemplate,\n                    inputLogStructure.eventTitleTemplate,\n                );\n            }\n            if (!shouldRegenerateLogEvents) {\n                shouldRegenerateLogEvents = (\n                    originalLogStructure.log_level !== updated.log_level\n                    || originalLogStructure.allow_event_details !== updated.allow_event_details\n                );\n            }\n            if (shouldRegenerateLogEvents) {\n                inputLogEvents = await this.invoke.call(\n                    this,\n                    'log-event-list',\n                    { where: { logStructure: inputLogStructure } },\n                );\n            }\n        }\n\n        const updatedLogStructure = await this.database.createOrUpdateItem('LogStructure', logStructure, updated);\n\n        await this.database.setEdges(\n            'LogStructureToLogTopic',\n            'source_structure_id',\n            updatedLogStructure.id,\n            'target_topic_id',\n            Object.values(targetLogTopicIDs).reduce((result, topicID) => {\n                // eslint-disable-next-line no-param-reassign\n                result[topicID] = {};\n                return result;\n            }, {}),\n        );\n\n        if (\n            !originalLogStructure\n            || inputLogStructure.eventKeys.some((eventKey) => isVirtualItem(eventKey))\n        ) {\n            // On creation of logStructure or update of eventKeys,\n            // replace the virtual IDs in the title template.\n            const originalItems = [inputLogStructure, ...inputLogStructure.eventKeys];\n            const updatedItems = originalItems.map((item, index) => ({\n                ...getPartialItem(item),\n                __id__: index || updatedLogStructure.id,\n            }));\n            const updatedTitleTemplate = RichTextUtils.updateDraftContent(\n                inputLogStructure.eventTitleTemplate,\n                originalItems,\n                updatedItems,\n            );\n            const transaction = this.database.getTransaction();\n            const fields2 = {\n                event_title_template: RichTextUtils.serialize(\n                    updatedTitleTemplate,\n                    RichTextUtils.StorageType.DRAFTJS,\n                ),\n            };\n            await updatedLogStructure.update(fields2, { transaction });\n        }\n\n        if (inputLogEvents) {\n            await asyncSequence(inputLogEvents, async (inputLogEvent) => {\n                // Update the logEvent to support eventKey addition, reorder, deletion.\n                const mapping = {};\n                inputLogEvent.logStructure.eventKeys.forEach((eventKey) => {\n                    mapping[eventKey.__id__] = eventKey;\n                });\n                inputLogEvent.logStructure = {\n                    ...inputLogStructure,\n                    eventKeys: inputLogStructure.eventKeys.map((eventKey) => ({\n                        ...eventKey,\n                        value: (mapping[eventKey.__id__] || eventKey).value,\n                    })),\n                };\n                return this.invoke.call(this, 'log-event-upsert', inputLogEvent);\n            });\n        }\n\n        await this.invoke.call(\n            this,\n            'structure-value-typeahead-index-$refresh',\n            { structure_id: updatedLogStructure.id },\n        );\n\n        this.broadcast('reminder-sidebar');\n        return updatedLogStructure.id;\n    }\n\n    static async delete(id) {\n        const logStructure = await this.database.deleteByPk('LogStructure', id);\n        DataTypeBase.broadcast.call(this, 'log-structure-list', logStructure, ['group_id']);\n        await this.invoke.call(\n            this,\n            'structure-value-typeahead-index-$refresh',\n            { structure_id: logStructure.id },\n        );\n        return { id: logStructure.id };\n    }\n}\n\nLogStructure.Frequency = LogStructureFrequency;\nLogStructure.LogLevel = LogLevel;\n\nexport default LogStructure;\n"
  },
  {
    "path": "src/common/data_types/LogStructureFrequency.js",
    "content": "import {\n    addDays, addYears,\n    compareAsc,\n    getDay,\n    isFriday, isMonday, isSaturday, isSunday,\n    setDate, setMonth,\n    subDays, subYears,\n} from 'date-fns';\n\nimport DateUtils from '../DateUtils';\nimport Enum from './enum';\n\nconst FrequencyRawOptions = [\n    {\n        value: 'everyday',\n        label: 'Everyday',\n        getPreviousMatch(date) {\n            return subDays(date, 1);\n        },\n        getNextMatch(date) {\n            return addDays(date, 1);\n        },\n    },\n    {\n        value: 'weekdays',\n        label: 'Weekdays',\n        getPreviousMatch(date) {\n            if (isMonday(date)) {\n                return subDays(date, 3);\n            } if (isSunday(date)) {\n                return subDays(date, 2);\n            }\n            return subDays(date, 1);\n        },\n        getNextMatch(date) {\n            if (isFriday(date)) {\n                return addDays(date, 3);\n            } if (isSaturday(date)) {\n                return addDays(date, 2);\n            }\n            return addDays(date, 1);\n        },\n    },\n    {\n        value: 'weekends',\n        label: 'Weekends',\n        getPreviousMatch(date) {\n            if (isSunday(date)) {\n                return subDays(date, 1);\n            }\n            return subDays(date, getDay(date));\n        },\n        getNextMatch(date) {\n            if (isSaturday(date)) {\n                return addDays(date, 1);\n            }\n            return addDays(date, 6 - getDay(date));\n        },\n    },\n    // TODO: Add more as needed.\n];\n\nDateUtils.DaysOfTheWeek.forEach((day, index) => {\n    FrequencyRawOptions.push({\n        value: day.toLowerCase(),\n        label: day,\n        getPreviousMatch(date) {\n            const diff = (getDay(date) - index + 7) % 7;\n            return subDays(date, diff || 7);\n        },\n        getNextMatch(date) {\n            const diff = (index - getDay(date) + 7) % 7;\n            return addDays(date, diff || 7);\n        },\n    });\n});\n\nfunction parseYearlyFrequencyArgs(args) {\n    let [month, dayOfTheMonth] = args.split('-');\n    month = parseInt(month, 10) - 1; // 0 = January\n    dayOfTheMonth = parseInt(dayOfTheMonth, 10);\n    return { month, dayOfTheMonth };\n}\n\nFrequencyRawOptions.push({\n    value: 'yearly',\n    label: 'Yearly',\n    getPreviousMatch(date, args) {\n        const { month, dayOfTheMonth } = parseYearlyFrequencyArgs(args);\n        let target = setDate(setMonth(date, month), dayOfTheMonth);\n        if (compareAsc(date, target) <= 0) {\n            target = subYears(target, 1);\n        }\n        return target;\n    },\n    getNextMatch(date, args) {\n        const { month, dayOfTheMonth } = parseYearlyFrequencyArgs(args);\n        let target = setDate(setMonth(date, month), dayOfTheMonth);\n        if (compareAsc(date, target) >= 0) {\n            target = addYears(target, 1);\n        }\n        return target;\n    },\n});\n\nexport default Enum(FrequencyRawOptions);\n"
  },
  {
    "path": "src/common/data_types/LogStructureGroup.js",
    "content": "import DataTypeBase from './base';\nimport { getVirtualID } from './utils';\nimport { validateNonEmptyString } from './validation';\n\nclass LogStructureGroup extends DataTypeBase {\n    static createVirtual() {\n        return {\n            __type__: 'log-structure-group',\n            __id__: getVirtualID(),\n            name: '',\n        };\n    }\n\n    static async updateWhere(where) {\n        await DataTypeBase.updateWhere.call(this, where, {\n            __id__: 'id',\n        });\n    }\n\n    static async validate(inputLogStructureGroup) {\n        const results = [];\n\n        results.push(validateNonEmptyString('.name', inputLogStructureGroup.name));\n\n        return results;\n    }\n\n    static async load(id) {\n        const logStructureGroup = await this.database.findByPk('LogStructureGroup', id);\n        return {\n            __type__: 'log-structure-group',\n            __id__: logStructureGroup.id,\n            name: logStructureGroup.name,\n        };\n    }\n\n    static async save(inputLogStructureGroup) {\n        const originalLogStructureGroup = await this.database.findItem(\n            'LogStructureGroup',\n            inputLogStructureGroup,\n        );\n        const orderingIndex = await DataTypeBase.getOrderingIndex.call(\n            this,\n            originalLogStructureGroup,\n        );\n        const fields = {\n            ordering_index: orderingIndex,\n            name: inputLogStructureGroup.name,\n        };\n        const updatedLogStructureGroup = await this.database.createOrUpdateItem('LogStructureGroup', originalLogStructureGroup, fields);\n        if (originalLogStructureGroup) {\n            await LogStructureGroup.updateLogStructures.call(this, inputLogStructureGroup);\n        }\n        this.broadcast('log-structure-group-list');\n        return updatedLogStructureGroup.id;\n    }\n\n    static async updateLogStructures(inputLogStructureGroup) {\n        const inputLogStructures = await this.invoke.call(\n            this,\n            'log-structure-list',\n            { where: { logStructureGroup: inputLogStructureGroup } },\n        );\n        await Promise.all(inputLogStructures.map(\n            async (inputLogStructure) => this.invoke.call(this, 'log-structure-upsert', inputLogStructure),\n        ));\n    }\n\n    static async delete(id) {\n        const logStructureGroup = await this.database.deleteByPk('LogStructureGroup', id);\n        this.broadcast('log-structure-group-list');\n        return { __id__: logStructureGroup.id };\n    }\n}\n\nexport default LogStructureGroup;\n"
  },
  {
    "path": "src/common/data_types/LogTopic.js",
    "content": "import { asyncSequence } from '../AsyncUtils';\nimport RichTextUtils from '../RichTextUtils';\nimport DataTypeBase from './base';\nimport LogKey from './LogKey';\nimport { getVirtualID } from './utils';\nimport { validateNonEmptyString, validateRecursiveList } from './validation';\n\nclass LogTopic extends DataTypeBase {\n    static createVirtual({ parentLogTopic = null, name = '' } = {}) {\n        if (parentLogTopic && parentLogTopic.childKeys) {\n            parentLogTopic.childKeys.forEach((inputLogKey) => {\n                if (!inputLogKey.isOptional) {\n                    inputLogKey.value = LogKey.Type[inputLogKey.type].getDefault(inputLogKey);\n                }\n            });\n        }\n        return {\n            __type__: 'log-topic',\n            __id__: getVirtualID(),\n            parentLogTopic,\n            name,\n            details: null,\n            childKeys: null,\n            childCount: 0,\n            isFavorite: false,\n            isDeprecated: false,\n        };\n    }\n\n    static async updateWhere(where) {\n        await DataTypeBase.updateWhere.call(this, where, {\n            __id__: 'id',\n            isFavorite: 'is_favorite',\n            isDeprecated: 'is_deprecated',\n            parentLogTopic: 'parent_topic_id',\n        });\n    }\n\n    static trigger(inputLogTopic) {\n        if (inputLogTopic.parentLogTopic && inputLogTopic.parentLogTopic.childNameTemplate) {\n            const { childKeys } = inputLogTopic.parentLogTopic;\n            inputLogTopic.name = RichTextUtils.extractPlainText(\n                RichTextUtils.updateDraftContent(\n                    inputLogTopic.parentLogTopic.childNameTemplate,\n                    childKeys,\n                    childKeys.map((logKey) => logKey.value || (logKey.isOptional ? '' : logKey)),\n                    true, // evaluateExpressions\n                ),\n            );\n        }\n        // Do nothing by default.\n    }\n\n    static async updateLogTopicInDetails(inputLogTopic) {\n        const originalLogTopics = Object.values(\n            RichTextUtils.extractMentions(inputLogTopic.details, 'log-topic'),\n        );\n        const updatedLogTopics = await Promise.all(\n            originalLogTopics.map((originalTopic) => this.invoke.call(\n                this,\n                'log-topic-load-partial',\n                originalTopic,\n            )),\n        );\n        inputLogTopic.details = RichTextUtils.updateDraftContent(\n            inputLogTopic.details,\n            originalLogTopics,\n            updatedLogTopics,\n        );\n        return updatedLogTopics.map((logTopic) => logTopic.__id__);\n    }\n\n    static async updateLogTopics(inputLogTopic) {\n        const promises = [];\n        promises.push(LogTopic.updateLogTopicInDetails.call(this, inputLogTopic));\n        if (inputLogTopic.parentLogTopic && inputLogTopic.parentLogTopic.childKeys) {\n            inputLogTopic.parentLogTopic.childKeys.forEach((inputLogKey) => {\n                promises.push(LogKey.updateLogTopics.call(this, inputLogKey));\n            });\n        }\n        const listOfTopicIDs = await Promise.all(promises);\n        return listOfTopicIDs.flat();\n    }\n\n    static async validate(inputLogTopic) {\n        const results = [];\n        results.push(validateNonEmptyString('.name', inputLogTopic.name));\n\n        if (inputLogTopic.childKeys) {\n            results.push(...await validateRecursiveList.call(\n                this,\n                LogKey,\n                '.childKeys',\n                inputLogTopic.childKeys,\n            ));\n        }\n\n        if (inputLogTopic.parentLogTopic && inputLogTopic.parentLogTopic.childKeys) {\n            const logKeyResults = await Promise.all(\n                inputLogTopic.parentLogTopic.childKeys.map(\n                    async (inputLogKey, index) => LogKey.validateValue.call(\n                        this,\n                        inputLogKey,\n                        index,\n                    ),\n                ),\n            );\n            results.push(...logKeyResults.filter((result) => result));\n        }\n\n        return results;\n    }\n\n    static async loadPartial(id) {\n        const logTopic = await this.database.findByPk('LogTopic', id);\n        return {\n            __type__: 'log-topic',\n            __id__: logTopic.id,\n            name: logTopic.name,\n        };\n    }\n\n    static async load(id) {\n        const logTopic = await this.database.findByPk('LogTopic', id);\n        let outputParentLogTopic = null;\n        if (logTopic.parent_topic_id) {\n            // Intentionally loading only partial data.\n            const parentLogTopic = await this.database.findByPk(\n                'LogTopic',\n                logTopic.parent_topic_id,\n            );\n            let outputParentChildKeys = null;\n            if (parentLogTopic.child_keys) {\n                outputParentChildKeys = await Promise.all(\n                    JSON.parse(parentLogTopic.child_keys).map(\n                        (logKey, index) => LogKey.load.call(this, logKey, index + 1),\n                    ),\n                );\n                const parentValues = JSON.parse(logTopic.parent_values);\n                outputParentChildKeys.forEach((logKey, index) => {\n                    logKey.value = parentValues[index] || null;\n                });\n            }\n            outputParentLogTopic = {\n                __type__: 'log-topic',\n                __id__: parentLogTopic.id,\n                name: parentLogTopic.name,\n                childKeys: outputParentChildKeys,\n                childNameTemplate: RichTextUtils.deserialize(\n                    parentLogTopic.child_name_template,\n                    RichTextUtils.StorageType.DRAFTJS,\n                ),\n            };\n        }\n        let outputChildKeys = null;\n        if (logTopic.child_keys) {\n            outputChildKeys = await Promise.all(\n                JSON.parse(logTopic.child_keys).map(\n                    (logKey, index) => LogKey.load.call(this, logKey, index + 1),\n                ),\n            );\n        }\n        return {\n            __type__: 'log-topic',\n            __id__: logTopic.id,\n            parentLogTopic: outputParentLogTopic,\n            name: logTopic.name,\n            details: RichTextUtils.deserialize(\n                logTopic.details,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            childKeys: outputChildKeys,\n            childNameTemplate: RichTextUtils.deserialize(\n                logTopic.child_name_template,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            childCount: logTopic.child_count,\n            isFavorite: logTopic.is_favorite,\n            isDeprecated: logTopic.is_deprecated,\n        };\n    }\n\n    static async save(inputLogTopic) {\n        let logTopic = await this.database.findItem('LogTopic', inputLogTopic);\n\n        const original = {};\n        if (logTopic) {\n            original.id = logTopic.id;\n            original.name = logTopic.name;\n            original.parent_topic_id = logTopic.parent_topic_id;\n            original.child_name_template = logTopic.child_name_template;\n            original.child_keys = logTopic.child_keys;\n        }\n\n        if (original.id && original.name !== inputLogTopic.name) {\n            // Update the name first, so that all referencing items\n            // that reference this topic can see the new name.\n            await this.database.update('LogTopic', {\n                id: original.id,\n                name: inputLogTopic.name,\n            });\n        }\n        // Before the serialization process, since the input is modified.\n        const targetLogTopicIDs = await LogTopic.updateLogTopics.call(this, inputLogTopic);\n\n        const orderingIndex = await DataTypeBase.getOrderingIndex.call(this, logTopic);\n        let childKeys;\n        if (inputLogTopic.childKeys) {\n            childKeys = inputLogTopic.childKeys.map(\n                (logKey) => LogKey.save.call(this, logKey),\n            );\n        }\n        let parentValues;\n        if (inputLogTopic.parentLogTopic && inputLogTopic.parentLogTopic.childKeys) {\n            parentValues = inputLogTopic.parentLogTopic.childKeys.map(\n                (logKey) => logKey.value || null,\n            );\n        }\n        const updated = {\n            parent_topic_id: inputLogTopic.parentLogTopic\n                ? inputLogTopic.parentLogTopic.__id__\n                : null,\n            ordering_index: orderingIndex,\n            name: inputLogTopic.name,\n            details: RichTextUtils.serialize(\n                inputLogTopic.details,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            child_keys: childKeys ? JSON.stringify(childKeys) : null,\n            child_name_template: RichTextUtils.serialize(\n                inputLogTopic.childNameTemplate,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            parent_values: parentValues ? JSON.stringify(parentValues) : null,\n            child_count: 'invalid', // will be set below\n            is_favorite: inputLogTopic.isFavorite,\n            is_deprecated: inputLogTopic.isDeprecated,\n        };\n\n        DataTypeBase.broadcast.call(\n            this,\n            'log-topic-list',\n            logTopic,\n            { parent_topic_id: updated.parent_topic_id },\n        );\n\n        let shouldUpdateChildTopics = false;\n        if (!shouldUpdateChildTopics && original.child_keys !== updated.child_keys) {\n            shouldUpdateChildTopics = true;\n        }\n        if (!shouldUpdateChildTopics) {\n            const originalChildNameTemplate = RichTextUtils.deserialize(\n                original.child_name_template,\n                RichTextUtils.StorageType.DRAFTJS,\n            );\n            if (!RichTextUtils.equals(originalChildNameTemplate, inputLogTopic.childNameTemplate)) {\n                shouldUpdateChildTopics = true;\n            }\n        }\n        let childLogTopics;\n        if (shouldUpdateChildTopics) {\n            childLogTopics = await this.invoke.call(\n                this,\n                'log-topic-list',\n                { where: { parentLogTopic: inputLogTopic } },\n            );\n            updated.child_count = childLogTopics.length;\n        } else {\n            updated.child_count = await LogTopic.count.call(\n                this,\n                { parent_topic_id: inputLogTopic.__id__ },\n            );\n        }\n\n        logTopic = await this.database.createOrUpdateItem('LogTopic', logTopic, updated);\n\n        await this.database.setEdges(\n            'LogTopicToLogTopic',\n            'source_topic_id',\n            logTopic.id,\n            'target_topic_id',\n            Object.values(targetLogTopicIDs).reduce((result, topicID) => {\n                // eslint-disable-next-line no-param-reassign\n                result[topicID] = {};\n                return result;\n            }, {}),\n        );\n\n        if (original.parent_topic_id !== updated.parent_topic_id) {\n            // Update counts on parent log topics.\n            const maybeUpdate = async (id) => {\n                if (!id) {\n                    return;\n                }\n                const parentLogTopic = await this.invoke.call(this, 'log-topic-load', { __id__: id });\n                await this.invoke.call(this, 'log-topic-upsert', parentLogTopic);\n            };\n            await Promise.all([\n                maybeUpdate(original.parent_topic_id),\n                maybeUpdate(updated.parent_topic_id),\n            ]);\n        }\n\n        if (original.id && original.name !== updated.name) {\n            // Update names on references items.\n            await Promise.all([\n                LogTopic.updateOtherEntities.call(\n                    this,\n                    'LogEventToLogTopic',\n                    logTopic.id,\n                    'source_event_id',\n                    'log-event',\n                ),\n                LogTopic.updateOtherEntities.call(\n                    this,\n                    'LogStructureToLogTopic',\n                    logTopic.id,\n                    'source_structure_id',\n                    'log-structure',\n                ),\n                LogTopic.updateOtherEntities.call(\n                    this,\n                    'LogTopicToLogTopic',\n                    logTopic.id,\n                    'source_topic_id',\n                    'log-topic',\n                ),\n            ]);\n        }\n\n        if (shouldUpdateChildTopics) {\n            await asyncSequence(childLogTopics, async (childLogTopic) => {\n                // Update the childLogTopics to support logKey addition, reorder, deletion.\n                const mapping = {};\n                if (childLogTopic.parentLogTopic.childKeys) {\n                    childLogTopic.parentLogTopic.childKeys.forEach((logKey) => {\n                        mapping[logKey.__id__] = logKey;\n                    });\n                }\n                childLogTopic.parentLogTopic = {\n                    ...inputLogTopic,\n                    childKeys: inputLogTopic.childKeys.map((logKey) => ({\n                        ...logKey,\n                        value: (mapping[logKey.__id__] || logKey).value,\n                    })),\n                };\n                return this.invoke.call(this, 'log-topic-upsert', childLogTopic);\n            });\n        }\n\n        await this.invoke.call(\n            this,\n            'topic-value-typeahead-index-$refresh',\n            { parent_topic_id: logTopic.parent_topic_id },\n        );\n\n        return logTopic.id;\n    }\n\n    static async updateOtherEntities(\n        junctionTableName,\n        targetTopicID,\n        junctionSourceName,\n        entityType,\n    ) {\n        const edges = await this.database.getEdges(\n            junctionTableName,\n            'target_topic_id',\n            targetTopicID,\n        );\n        const inputItems = await Promise.all(\n            edges.map((edge) => this.invoke.call(\n                this,\n                `${entityType}-load`,\n                { __id__: edge[junctionSourceName] },\n            )),\n        );\n        await Promise.all(inputItems.map(\n            (inputItem) => this.invoke.call(this, `${entityType}-upsert`, inputItem),\n        ));\n    }\n\n    static async delete(id) {\n        const logTopic = await this.database.deleteByPk('LogTopic', id);\n        if (logTopic.parent_topic_id) {\n            const parentLogTopic = await this.invoke.call(\n                this,\n                'log-topic-load',\n                { __id__: logTopic.parent_topic_id },\n            );\n            await this.invoke.call(this, 'log-topic-upsert', parentLogTopic);\n        }\n\n        DataTypeBase.broadcast.call(this, 'log-topic-list', logTopic, ['parent_topic_id']);\n        await this.invoke.call(\n            this,\n            'topic-value-typeahead-index-$refresh',\n            { parent_topic_id: logTopic.parent_topic_id },\n        );\n        return { __id__: logTopic.id };\n    }\n\n    static async sort(input) {\n        const items = await this.database.findAll(\n            this.DataType.name,\n            input.where,\n            [['name', 'ASC']],\n            null, // limit\n        );\n        await Promise.all(items.map(\n            (item, index) => this.database.update(\n                this.DataType.name,\n                { id: item.id, ordering_index: index },\n            ),\n        ));\n        this.broadcast(`${input.dataType}-list`, { where: input.where });\n    }\n}\n\nexport default LogTopic;\n"
  },
  {
    "path": "src/common/data_types/__tests__/LogStructureFrequency.test.js",
    "content": "import { addDays, compareAsc } from 'date-fns';\n\nimport DateUtils from '../../DateUtils';\nimport LogStructureFrequency from '../LogStructureFrequency';\n\ntest('test_previous_and_next_match_methods', async () => {\n    // Verify the symmetry of the 2 frequency methods.\n    const { todayDate } = DateUtils.getContext();\n    LogStructureFrequency.Options.forEach((frequencyOption) => {\n        if (frequencyOption.value === LogStructureFrequency.YEARLY) {\n            return;\n        }\n        for (let offset = 0; offset < 7; offset += 1) {\n            const startDate = addDays(todayDate, offset);\n            const forwardDate = frequencyOption.getNextMatch(startDate);\n            const middleDate = frequencyOption.getPreviousMatch(forwardDate);\n            const backwardDate = frequencyOption.getPreviousMatch(middleDate);\n            const endDate = frequencyOption.getNextMatch(backwardDate);\n            expect(compareAsc(middleDate, endDate)).toEqual(0);\n        }\n    });\n\n    function check(frequency, date1, method, date2, args = null) {\n        const result = LogStructureFrequency[frequency][method](DateUtils.getDate(date1), args);\n        expect(DateUtils.getLabel(result)).toEqual(date2);\n    }\n    check('everyday', '2020-07-29', 'getPreviousMatch', '2020-07-28');\n    check('everyday', '2020-07-29', 'getNextMatch', '2020-07-30');\n    check('weekdays', '2020-08-03', 'getPreviousMatch', '2020-07-31');\n    check('weekdays', '2020-08-04', 'getPreviousMatch', '2020-08-03');\n    check('weekends', '2020-07-28', 'getNextMatch', '2020-08-01');\n    check('weekends', '2020-08-01', 'getNextMatch', '2020-08-02');\n    check('thursday', '2020-08-01', 'getPreviousMatch', '2020-07-30');\n    check('thursday', '2020-07-30', 'getNextMatch', '2020-08-06');\n    check('yearly', '2020-08-15', 'getPreviousMatch', '2020-08-12', '08-12');\n    check('yearly', '2020-08-12', 'getNextMatch', '2021-08-12', '08-12');\n});\n"
  },
  {
    "path": "src/common/data_types/api.js",
    "content": "export default class DataTypeAPI {\n    // The process of adding a new data type should be consistent.\n\n    static createVirtual() { // create\n        // Usage? Client.\n        // Used by the client when it wants to create a new object of this type.\n        throw new Error('not implemented');\n    }\n\n    static trigger(_item) { // update\n        // Usage? Client & Server.\n        // Invoked whenever the object is updated by any user action.\n        throw new Error('not implemented');\n    }\n\n    static async validate(_item) {\n        // Usage? Client & Server.\n        // Take the canonical representation and return a list of violations.\n        throw new Error('not implemented');\n    }\n\n    static async where(_fields) { // filter, search\n        // Usage? Server.\n        // Translate a client-side query into a valid database query.\n        throw new Error('not implemented');\n    }\n\n    static async load(_id) {\n        // Usage? Server.\n        // Load the object from the database, and build the canonical representation.\n        throw new Error('not implemented');\n    }\n\n    static async save(_item) {\n        // Usage? Server.\n        // Take the canonical representation and write it to the database.\n        throw new Error('not implemented');\n    }\n\n    static async delete(_id) {\n        // Usage? Server.\n        // Delete the specified object from the database.\n        throw new Error('not implemented');\n    }\n}\n"
  },
  {
    "path": "src/common/data_types/base.js",
    "content": "import assert from 'assert';\n\nimport { asyncSequence } from '../AsyncUtils';\nimport DataTypeAPI from './api';\nimport { isItem } from './utils';\n\nfunction getDataType(name) {\n    return name.split(/(?=[A-Z])/).map((word) => word.toLowerCase()).join('-');\n}\n\nexport default class DataTypeBase extends DataTypeAPI {\n    static async getValidationErrors(inputItem) {\n        const { DataType } = this;\n        const result = await DataType.validate.call(this, inputItem);\n        let prefix = DataType.name;\n        prefix = prefix[0].toLowerCase() + prefix.substring(1);\n        for (let jj = 0; jj < result.length; jj += 1) {\n            result[jj][0] = prefix + result[jj][0];\n        }\n        return result\n            .filter((item) => !item[1])\n            .map((item) => `${item[0]} ${item[2]}`);\n    }\n\n    static async updateLogTopicsWhere(where) {\n        // Special case! The logTopics filter is handled via junction tables,\n        // unlike the remaining fields that can be queried normally.\n        const junctionTableName = `${this.DataType.name}ToLogTopic`;\n        const junctionSourceName = {\n            LogTopic: 'source_topic_id',\n            LogStructure: 'source_structure_id',\n            LogEvent: 'source_event_id',\n        }[this.DataType.name];\n        assert(junctionSourceName);\n        const logTopicIds = where.logTopics.map((item) => item.__id__);\n        const edges = await this.database.getEdges(\n            junctionTableName,\n            'target_topic_id',\n            logTopicIds,\n        );\n        let itemIds;\n        if (logTopicIds.length > 1) {\n            // assuming AND operation, not OR\n            const counters = {};\n            edges.forEach((edge) => {\n                const id = edge[junctionSourceName];\n                counters[id] = (counters[id] || 0) + 1;\n            });\n            itemIds = Object.entries(counters)\n                .filter((pair) => pair[1] === logTopicIds.length)\n                .map((pair) => parseInt(pair[0], 10));\n        } else {\n            itemIds = edges.map((edge) => edge[junctionSourceName]);\n        }\n        delete where.logTopics;\n        assert(!where.id);\n        where.id = itemIds;\n    }\n\n    static async updateWhere(where, mapping) {\n        await asyncSequence(Object.keys(where), async (fieldName) => {\n            if (fieldName === 'logTopics') {\n                await DataTypeBase.updateLogTopicsWhere.call(this, where);\n            } else if (fieldName in mapping) {\n                const newFieldName = mapping[fieldName];\n                let value = where[fieldName];\n                value = isItem(value) ? value.__id__ : value;\n                where[newFieldName] = value;\n                if (fieldName !== newFieldName) {\n                    delete where[fieldName];\n                }\n            } else {\n                assert(false, `undefined where mapping: ${fieldName}`);\n            }\n        });\n    }\n\n    static trigger(item) {\n        // Do nothing by default.\n    }\n\n    static async list(where, limit) {\n        const order = [['ordering_index', 'DESC']];\n        if (this.DataType.name === 'LogEvent') {\n            order.unshift(['date', 'DESC']);\n        }\n        const items = await this.database.findAll(this.DataType.name, where, order, limit);\n        return Promise.all(\n            items.reverse().map((item) => this.DataType.load.call(this, item.id)),\n        );\n    }\n\n    static async count(where) {\n        return this.database.count(this.DataType.name, where);\n    }\n\n    // eslint-disable-next-line no-unused-vars\n    static async typeahead({ query, where }) {\n        if (\n            {\n                LogTopic: true,\n                LogStructure: true,\n            }[this.DataType.name]\n        ) {\n            where = { ...where, is_deprecated: false };\n        }\n        const options = await this.database.findAll(\n            this.DataType.name,\n            { ...where, name: { [this.database.Op.substring]: query } },\n        );\n        const dataType = getDataType(this.DataType.name);\n        const items = options.map((option) => ({\n            __type__: dataType,\n            __id__: option.id,\n            name: option.name,\n        })).sort((left, right) => left.name.localeCompare(right.name));\n        const first = [];\n        const second = [];\n        const third = [];\n        query = query.toLowerCase();\n        items.forEach((item) => {\n            if (item.name === query) {\n                first.push(item);\n            } else if (item.name.toLowerCase().startsWith(query)) {\n                second.push(item);\n            } else { // item.name.toLowerCase().includes(query)\n                third.push(item);\n            }\n        });\n        return [...first, ...second, ...third];\n    }\n\n    // eslint-disable-next-line no-unused-vars\n    static async reorder(input) {\n        // The client-side does not know the underscore names used in the database.\n        // Is it possible to add a mysql index to prevent conflicts?\n        const items = await Promise.all(input.ordering.map(\n            (id, index) => this.database.update(\n                this.DataType.name,\n                { id, ordering_index: index },\n            ),\n        ));\n        this.broadcast(`${input.dataType}-list`, { where: input.where });\n        return items.map((item) => item.id);\n    }\n\n    static async getOrderingIndex(item, where = {}) {\n        if (item) {\n            return item.ordering_index;\n        }\n        return this.database.count(this.DataType.name, where, null);\n    }\n\n    static async broadcast(queryName, prevItem, fields) {\n        if (!this.DataType) {\n            return;\n        }\n        if (Array.isArray(fields)) {\n            fields.forEach((fieldName) => {\n                const prevValue = prevItem ? prevItem[fieldName] : null;\n                this.broadcast(queryName, { where: { [fieldName]: prevValue } });\n            });\n        } else {\n            Object.entries(fields).forEach(([fieldName, nextValue]) => {\n                if (prevItem) {\n                    const prevValue = prevItem[fieldName];\n                    this.broadcast(queryName, { where: { [fieldName]: prevValue } });\n                }\n                this.broadcast(queryName, { where: { [fieldName]: nextValue } });\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/common/data_types/enum.js",
    "content": "import assert from 'assert';\n\nexport default function Enum(Options) {\n    const result = { Options };\n    Options.forEach((option) => {\n        assert(option.value === option.value.toLowerCase());\n        const key = option.value.toUpperCase().replace(/-/g, '_');\n        result[key] = option.value;\n        result[option.value] = option;\n    });\n    return result;\n}\n"
  },
  {
    "path": "src/common/data_types/index.js",
    "content": "import LogEvent from './LogEvent';\nimport LogKey from './LogKey';\nimport LogStructure from './LogStructure';\nimport LogStructureGroup from './LogStructureGroup';\nimport LogTopic from './LogTopic';\n\nexport {\n    LogTopic,\n    LogStructureGroup,\n    LogStructure,\n    LogKey,\n    LogEvent,\n};\n\nconst Mapping = {\n    'log-topic': LogTopic,\n    'log-structure-group': LogStructureGroup,\n    'log-structure': LogStructure,\n    'log-event': LogEvent,\n};\n\nexport function getDataTypeMapping() {\n    return Mapping;\n}\n\nexport { default as Enum } from './enum';\nexport * from './utils';\n"
  },
  {
    "path": "src/common/data_types/utils.js",
    "content": "let virtualID = 0;\n\nexport function getVirtualID() {\n    virtualID -= 1;\n    return virtualID;\n}\n\nexport function isItem(item) {\n    return item && typeof item.__id__ === 'number';\n}\n\nexport function isVirtualItem(item) {\n    return item && item.__id__ < 0;\n}\n\nexport function isRealItem(item) {\n    return item && item.__id__ > 0;\n}\n\nexport function getPartialItem(item) {\n    return item ? { __type__: item.__type__, __id__: item.__id__, name: item.name } : null;\n}\n\nexport function getNextID(items) {\n    let nextId = -1;\n    // eslint-disable-next-line no-loop-func\n    while (items.some((item) => item.__id__ === nextId)) {\n        nextId -= 1;\n    }\n    return nextId;\n}\n"
  },
  {
    "path": "src/common/data_types/validation.js",
    "content": "// A collection utilities used in the validate() methods of different data types.\n\nexport function validateNonEmptyString(name, value) {\n    if (typeof value !== 'string') {\n        return [\n            name,\n            false,\n            'must be a string.',\n        ];\n    }\n    return [\n        name,\n        value.length > 0,\n        'must be non-empty.',\n    ];\n}\n\nexport function validateIndex(name, value) {\n    if (typeof value !== 'number') {\n        return [\n            name,\n            false,\n            'must be a number.',\n        ];\n    }\n    return [\n        name,\n        value > 0,\n        'must be >= 0.',\n    ];\n}\n\nexport function validateEnumValue(name, value, Enum) {\n    return [\n        name,\n        !!Enum[value],\n        'must be valid.',\n    ];\n}\n\nexport function validateDateLabel(name, label) {\n    return [\n        name,\n        !!label.match(/^\\d{4}-\\d{2}-\\d{2}$/),\n        'is an invalid date.',\n    ];\n}\n\nexport async function validateRecursive(DataType, name, item) {\n    const result = await DataType.validate.call(this, item);\n    const prefix = name;\n    for (let jj = 0; jj < result.length; jj += 1) {\n        result[jj][0] = prefix + result[jj][0];\n    }\n    return result;\n}\n\nexport async function validateRecursiveList(DataType, name, items) {\n    if (!Array.isArray(items)) {\n        return [\n            name,\n            false,\n            `must be an Array<${DataType.name}>`,\n        ];\n    }\n    const results = await Promise.all(\n        items.map((item) => DataType.validate.call(this, item)),\n    );\n    for (let ii = 0; ii < results.length; ii += 1) {\n        const prefix = `${name}[${ii}]`;\n        for (let jj = 0; jj < results[ii].length; jj += 1) {\n            results[ii][jj][0] = prefix + results[ii][jj][0];\n        }\n    }\n    return results.flat();\n}\n"
  },
  {
    "path": "src/common/polyfill.js",
    "content": "function getTimePrefix() {\n    const now = new Date();\n    return `[${now.toLocaleDateString()} ${now.toLocaleTimeString()}]`;\n}\n\nfunction addTimePrefix(name) {\n    // eslint-disable-next-line no-console\n    const original = console[name];\n    // eslint-disable-next-line no-console\n    console[name] = (...args) => {\n        original(getTimePrefix(), ...args);\n    };\n}\n\nif (typeof global === 'object') {\n    addTimePrefix('error');\n    addTimePrefix('info');\n    addTimePrefix('log');\n    addTimePrefix('warning');\n}\n"
  },
  {
    "path": "src/demo/components/Application.js",
    "content": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport DetailsSection from './DetailsSection';\nimport IndexSection from './IndexSection';\nimport ModalDialog from './ModalDialog';\nimport SidebarSection from './SidebarSection';\n\nexport default class Application extends BaseWrapper {\n    constructor(webdriver) {\n        super(webdriver, true);\n        this.webdriver = webdriver;\n    }\n\n    async switchToTab(name) {\n        const element = await this.webdriver.findElement(\n            By.xpath(`//div[contains(@class, 'sidebar-section')]/div[text() = '${name}']`),\n        );\n        await this.moveToAndClick(element);\n    }\n\n    async getSidebarSection(...args) {\n        return SidebarSection.get(this.webdriver, ...args);\n    }\n\n    async getIndexSection() {\n        await this.waitUntil(async () => IndexSection.get(this.webdriver));\n        return IndexSection.get(this.webdriver);\n    }\n\n    async getDetailsSection(...args) {\n        return DetailsSection.get(this.webdriver, ...args);\n    }\n\n    async getModalDialog(...args) {\n        return ModalDialog.get(this.webdriver, ...args);\n    }\n\n    async getTopic(name, index) {\n        const elements = await this.webdriver.findElements(By.xpath(`//a[contains(@class, 'topic') and text() = '${name}']`));\n        const element = BaseWrapper.getItemByIndex(elements, index);\n        return new BaseWrapper(this.webdriver, element);\n    }\n\n    async getLink(name, index = 0) {\n        const elements = await this.webdriver.findElements(By.xpath(`//a[text() = '${name}']`));\n        const element = BaseWrapper.getItemByIndex(elements, index);\n        return new BaseWrapper(this.webdriver, element);\n    }\n\n    // Random Specific Items\n\n    async isDetailsSectionActive() {\n        const detailSection = await this.getDetailsSection(0);\n        return detailSection.isActive();\n    }\n\n    async performCreateNew(bulletList) {\n        const headerItem = await bulletList.getHeader();\n        await headerItem.perform('Create New');\n        await this.waitUntil(async () => !!(await this.getModalDialog(0)));\n        return this.getModalDialog(0);\n    }\n\n    async performInputName(name) {\n        const modalDialog = await this.getModalDialog(0);\n        const nameInput = await modalDialog.getTextInput('Name');\n        await nameInput.typeSlowly(name);\n        await modalDialog.performSave();\n    }\n\n    async clearDatabase() {\n        await this.webdriver.executeScript(\"return window.api.send('database-clear')\");\n    }\n\n    // General Utility\n\n    async waitUntil(conditionMethod) {\n        await this.webdriver.wait(conditionMethod);\n        await this.wait();\n    }\n\n    async scrollToBottom(className, index) {\n        // Required for lower resolution demo videos.\n        const injectedMethod = (innerClassName, innerIndex) => {\n            const node = document.getElementsByClassName(innerClassName)[innerIndex];\n            let prevScrollTop = null;\n            return (function loop() {\n                if (prevScrollTop === node.scrollTop) {\n                    return Promise.resolve();\n                }\n                prevScrollTop = node.scrollTop;\n                node.scrollBy(0, 10);\n                return new Promise((resolve) => {\n                    setTimeout(() => resolve(loop()), 10);\n                });\n            }());\n        };\n        await this.webdriver.executeScript(`return (${injectedMethod.toString()})(${JSON.stringify(className)}, ${index});`);\n    }\n}\n"
  },
  {
    "path": "src/demo/components/BaseWrapper.js",
    "content": "import assert from 'assert';\nimport { By, Key } from 'selenium-webdriver';\n\nimport { asyncSequence } from '../../common/AsyncUtils';\n\nexport default class BaseWrapper {\n    constructor(webdriver, element) {\n        assert(element, 'missing element');\n        this.webdriver = webdriver;\n        this.element = element;\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    async getInput() {\n        // Override this method to forward actions to the returned element.\n        return null;\n    }\n\n    async sendKeys(...items) {\n        const redirectInput = await this.getInput();\n        if (redirectInput) {\n            await redirectInput.sendKeys(...items);\n            return;\n        }\n        await asyncSequence(items, async (item) => {\n            let keys;\n            // Do not require application logic to use Selenium API.\n            if (typeof item === 'string') {\n                keys = Key[item];\n            } else if (Array.isArray(item)) {\n                keys = Key.chord(...item.map((key) => Key[key]));\n            } else {\n                assert(false, `invalid item: ${item}`);\n            }\n            await this.element.sendKeys(keys);\n        });\n        await this.wait();\n    }\n\n    async typeSlowly(text) {\n        const redirectInput = await this.getInput();\n        if (redirectInput) {\n            await redirectInput.typeSlowly(text);\n            return;\n        }\n        await asyncSequence(Array.from(text), async (char) => {\n            await this.element.sendKeys(char);\n            // Dont need to add additional delay here.\n        });\n        await this.wait();\n    }\n\n    async _moveTo(element) {\n        await this.webdriver.actions().move({ origin: element || this.element }).perform();\n    }\n\n    async moveTo(element) {\n        await this._moveTo(element);\n        await this.wait();\n    }\n\n    async _click(element) {\n        await this.webdriver.actions().click(element || this.element).perform();\n    }\n\n    async click(element) {\n        await this._click(element);\n        await this.wait();\n    }\n\n    async moveToAndClick(element) {\n        await this.moveTo(element);\n        await this.click(element);\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    wait(milliseconds = 250) {\n        return new Promise((resolve) => {\n            setTimeout(resolve, milliseconds);\n        });\n    }\n\n    static getItemByIndex(items, index) {\n        return items[index < 0 ? items.length + index : index];\n    }\n\n    static async getElementByClassName(element, className, index = 0) {\n        const classAttribute = await element.getAttribute('class');\n        if (classAttribute.includes(className)) {\n            return element;\n        }\n        const elements = await element.findElements(By.className(className));\n        return elements[index] || null;\n    }\n}\n"
  },
  {
    "path": "src/demo/components/BulletList.js",
    "content": "/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\nimport { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport { TextEditor } from './Inputs';\n\nexport default class BulletList extends BaseWrapper {\n    static async get(webdriver, index) {\n        const elements = await webdriver.findElements(By.className('bullet-list'));\n        const element = BaseWrapper.getItemByIndex(elements, index);\n        return element ? new this(webdriver, element) : null;\n    }\n\n    async getHeader() {\n        const element = await this.element.findElement(By.xpath('./div[1]'));\n        // eslint-disable-next-line no-use-before-define\n        return new BulletListItem(this.webdriver, element);\n    }\n\n    async _getItems() {\n        return this.element.findElements(By.xpath(\"./div[2]/div[contains(@class, 'highlightable')]\"));\n    }\n\n    async getItem(index) {\n        const elements = await this._getItems();\n        // eslint-disable-next-line no-use-before-define\n        return new BulletListItem(this.webdriver, BaseWrapper.getItemByIndex(elements, index));\n    }\n\n    async getItemCount() {\n        const elements = await this._getItems();\n        return elements.length;\n    }\n\n    async getAdder() {\n        const element = this.element.findElement(By.xpath('./div[3]'));\n        return TextEditor.get(this.webdriver, element);\n    }\n}\n\nclass BulletListItem extends BaseWrapper {\n    async _getButton(title) {\n        await this._moveTo(this.element);\n        const button = this.element.findElement(By.xpath(\n            `.//div[contains(@class, 'icon') and @title='${title}']`,\n        ));\n        await this._moveTo(button);\n        return button;\n    }\n\n    async perform(name) {\n        const button = await this._getButton(name);\n        await this.moveToAndClick(button);\n    }\n\n    async performAction(name) {\n        await this._moveTo(this.element);\n        const actionButton = await this._getButton('Actions');\n        await this._moveTo(actionButton);\n        await this.webdriver.wait(async () => (await actionButton.findElements(By.className('dropdown-item'))).length > 0);\n        const actionElement = await actionButton.findElement(\n            By.xpath(`.//a[contains(@class, 'dropdown-item') and text() = '${name}']`),\n        );\n        await this.moveToAndClick(actionElement);\n    }\n\n    async getSubList() {\n        const items = await this.element.findElements(By.xpath(\n            './following-sibling::*[1]'\n            + \"//div[contains(@class, 'bullet-list')]\",\n        ));\n        return items.length ? new BulletList(this.webdriver, items[0]) : null;\n    }\n\n    async move(direction) {\n        assert(['UP', 'DOWN'].includes(direction));\n        const reorderButton = await this._getButton('Reorder');\n        await this._moveTo(reorderButton);\n        await this.wait();\n        await this.sendKeys(['SHIFT', direction]);\n    }\n}\n"
  },
  {
    "path": "src/demo/components/DetailsSection.js",
    "content": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport { TextEditor } from './Inputs';\n\nexport default class DetailsSection extends BaseWrapper {\n    static async get(webdriver, index) {\n        const elements = await webdriver.findElements(By.className('details-section'));\n        const element = BaseWrapper.getItemByIndex(elements, index);\n        return element ? new this(webdriver, element) : null;\n    }\n\n    async isActive() {\n        const elements = await this.element.findElements(By.xpath('.//input[@placeholder = \\'Details ...\\']'));\n        return elements.length === 0;\n    }\n\n    async getInput() {\n        return TextEditor.get(this.webdriver, this.element);\n    }\n\n    async perform(name) {\n        const button = await this.element.findElement(By.xpath(`.//button[@title = '${name}']`));\n        await this.moveToAndClick(button);\n    }\n}\n"
  },
  {
    "path": "src/demo/components/IndexSection.js",
    "content": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport BulletList from './BulletList';\nimport { TypeaheadSelector } from './Inputs';\n\nexport default class IndexSection extends BaseWrapper {\n    static async get(webdriver) {\n        const elements = await webdriver.findElements(By.className('index-section'));\n        return elements.length ? new this(webdriver, elements[0]) : null;\n    }\n\n    async getTypeahead() {\n        const inputElement = await this.element.findElement(\n            By.xpath(\"./div[1]//div[contains(@class, 'rbt')]\"),\n        );\n        return new TypeaheadSelector(this.webdriver, inputElement);\n    }\n\n    async getBulletList(index) {\n        const items = await this.element.findElements(By.xpath(\n            \"./div[contains(@class, 'scrollable-section')]\"\n            + \"/div[contains(@class, 'bullet-list')]\",\n        ));\n        const item = BaseWrapper.getItemByIndex(items, index);\n        return item ? new BulletList(this.webdriver, item) : null;\n    }\n}\n"
  },
  {
    "path": "src/demo/components/Inputs.js",
    "content": "/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\nimport { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\n\nexport class Selector extends BaseWrapper {\n    static async get(webdriver, element) {\n        const actual = await BaseWrapper.getElementByClassName(element, 'selector');\n        return actual ? new this(webdriver, actual) : null;\n    }\n\n    async pickOption(name) {\n        /*\n        const optionElements = await this.element.findElements(By.tagName('option'));\n        const optionLabels = await Promise.all(optionElements.map(element => element.getText()));\n        const index = optionLabels.findIndex(optionLabel => optionLabel === name);\n        await this.moveToAndClick(optionElements[index]);\n        // Error = [object HTMLOptionElement] has no size and location\n        */\n        await this.element.sendKeys(name);\n        await this.wait();\n    }\n}\n\nexport class TypeaheadSelector extends BaseWrapper {\n    static async get(webdriver, element) {\n        const actual = await BaseWrapper.getElementByClassName(element, 'rbt');\n        return actual ? new this(webdriver, actual) : null;\n    }\n\n    async getTokens() {\n        const items = await this.element.findElements(By.xpath(\".//div[contains(@class, 'rbt-token')]\"));\n        return Promise.all(items.map(async (token) => {\n            const names = await token.getText();\n            return names.split('\\n')[0];\n        }));\n    }\n\n    async removeToken(name) {\n        const removeButton = await this.element.findElement(By.xpath(\n            `.//div[contains(@class, 'rbt-token') and text() = '${name}']`\n            + '/button[contains(@class, \\'rbt-close\\')]',\n        ));\n        await this.moveToAndClick(removeButton);\n    }\n\n    async getInput() {\n        const wrappers = await this.element.findElements(By.xpath(\".//div[contains(@class, 'rbt-input-wrapper')]\"));\n        if (wrappers.length) {\n            // multi-selector\n            const inputElement = await wrappers[0].findElement(By.xpath('.//input[1]'));\n            return new BaseWrapper(this.webdriver, inputElement);\n        }\n        // single-selector\n        const inputElement = await this.element.findElement(By.xpath('./div[1]/input[1]'));\n        return new BaseWrapper(this.webdriver, inputElement);\n    }\n\n    async _getSuggestions() {\n        const elements = await this.element.findElements(By.xpath(\n            \".//div[contains(@class, 'menu-options') or contains(@class, 'rbt-menu')]\"\n            + \"/a[contains(@class, 'dropdown-item')]\",\n        ));\n        const names = await Promise.all(elements.map((token) => token.getText()));\n        return { elements, names };\n    }\n\n    async pickSuggestion(label) {\n        await this.webdriver.wait(async () => {\n            const { names } = await this._getSuggestions();\n            return names.some((item) => item.startsWith(label));\n        });\n        const { elements, names } = await this._getSuggestions();\n        const index = names.findIndex((item) => item.startsWith(label));\n        await this.moveToAndClick(elements[index]);\n    }\n}\n\nexport class TextEditor extends BaseWrapper {\n    static async get(webdriver, element) {\n        const actual = await BaseWrapper.getElementByClassName(element, 'text-editor');\n        return actual ? new this(webdriver, actual) : null;\n    }\n\n    async getInput() {\n        return new BaseWrapper(this.webdriver, this.element.findElement(\n            By.xpath(\".//div[contains(@class, 'public-DraftEditor-content')]\"),\n        ));\n    }\n\n    async getSuggestions() {\n        const tokens = await this.element.findElements(\n            By.xpath(\".//div[contains(@class, 'mention-suggestions')]/div/div\"),\n        );\n        return Promise.all(tokens.map((token) => token.getText()));\n    }\n\n    async pickSuggestion(indexOrLabel) {\n        await this.webdriver.wait(async () => (await this.getSuggestions()).length > 0);\n        await this.wait();\n        const offset = typeof indexOrLabel === 'number'\n            ? indexOrLabel\n            : (await this.getSuggestions()).indexOf(indexOrLabel);\n        assert(offset !== -1);\n        for (let ii = 1; ii < offset; ii += 1) {\n            // eslint-disable-next-line no-await-in-loop\n            await this.sendKeys('DOWN');\n        }\n        await this.sendKeys('ENTER');\n    }\n}\n\nexport class LogStructureKey extends BaseWrapper {\n    static async get(webdriver, element, index) {\n        const containers = await element.findElements(By.xpath('.//div[contains(@class, \\'log-structure-key\\')]'));\n        const container = BaseWrapper.getItemByIndex(containers, index);\n        return new this(webdriver, container);\n    }\n\n    async getTypeSelector() {\n        return Selector.get(this.webdriver, this.element, 0);\n    }\n\n    async getNameInput() {\n        return new BaseWrapper(this.webdriver, await this.element.findElement(By.tagName('input')));\n    }\n\n    async getTemplateInput() {\n        return new TextEditor(this.webdriver, this.element);\n    }\n}\n"
  },
  {
    "path": "src/demo/components/ModalDialog.js",
    "content": "import assert from 'assert';\nimport { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport {\n    LogStructureKey, Selector, TextEditor, TypeaheadSelector,\n} from './Inputs';\n\nexport default class ModalDialog extends BaseWrapper {\n    static async get(webdriver, index) {\n        const elements = await webdriver.findElements(By.className('modal-dialog'));\n        const element = BaseWrapper.getItemByIndex(elements, index);\n        return element ? new this(webdriver, element) : null;\n    }\n\n    async _clickAndWaitForClose(buttonElement) {\n        await this.webdriver.wait(async () => buttonElement.isEnabled());\n        await this.click(buttonElement);\n        await this.webdriver.wait(async () => {\n            try {\n                await this.element.isDisplayed();\n                return false;\n            } catch (error) {\n                assert(error.name === 'StaleElementReferenceError');\n                return true;\n            }\n        });\n        await this.wait();\n    }\n\n    async performClose() {\n        const buttonElement = await this.element.findElement(By.xpath(\n            '//div[contains(@class, \\'modal-content\\')]/div[1]'\n            + '//button',\n        ));\n        await this._clickAndWaitForClose(buttonElement);\n    }\n\n    async _getElement(name) {\n        return this.element.findElement(By.xpath(\n            '//div[contains(@class, \\'modal-content\\')]/div[2]'\n            + '//div[contains(@class, \\'input-group\\')]'\n            + `/span[contains(@class, 'input-group-text') and text() = '${name}']`\n            + '/../*[2]',\n        ));\n    }\n\n    async getTextInput(name) {\n        const element = await this._getElement(name);\n        return new BaseWrapper(this.webdriver, element);\n    }\n\n    async getTextEditor(name) {\n        const element = await this._getElement(name);\n        return TextEditor.get(this.webdriver, element);\n    }\n\n    async getTypeahead(name) {\n        const element = await this._getElement(name);\n        return TypeaheadSelector.get(this.webdriver, element);\n    }\n\n    async getSelector(name) {\n        const element = await this._getElement(name);\n        return Selector.get(this.webdriver, element);\n    }\n\n    async performSave() {\n        const buttonElement = await this.element.findElement(By.xpath(\n            '//div[contains(@class, \\'modal-content\\')]/div[3]'\n            + '//button[text() = \\'Save\\']',\n        ));\n        await this._clickAndWaitForClose(buttonElement);\n    }\n\n    // Methods specific to Log Structures.\n\n    async addLogStructureKey() {\n        const button = await this.element.findElement(By.xpath(\n            \".//button[contains(@class, 'log-structure-add-key')]\",\n        ));\n        await this.moveToAndClick(button);\n        return this.getLogStructureKey(-1);\n    }\n\n    async getLogStructureKey(index) {\n        return LogStructureKey.get(this.webdriver, this.element, index);\n    }\n\n    // Methods specific to Debug Info\n\n    async getDebugInfo() {\n        const element = await this.element.findElement(By.tagName('pre'));\n        return element.getText();\n    }\n}\n"
  },
  {
    "path": "src/demo/components/ReminderItem.js",
    "content": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\n\nexport default class ReminderItem extends BaseWrapper {\n    async getCheckbox() {\n        const checkbox = await this.element.findElement(By.xpath(\".//input[@type = 'checkbox']\"));\n        return new BaseWrapper(this.webdriver, checkbox);\n    }\n\n    async pickMenuItem(label) {\n        await this.moveTo();\n        const rightElement = await this.element.findElement(By.xpath(\".//div[contains(@class, 'icon')]\"));\n        await this.moveTo(rightElement);\n        await this.wait();\n        await this.webdriver.wait(async () => (await this.element.findElements(By.className('dropdown-item'))).length > 0);\n        const optionElement = await this.element.findElement(\n            By.xpath(`.//a[contains(@class, 'dropdown-item') and text() = '${label}']`),\n        );\n        await this.moveToAndClick(optionElement);\n    }\n}\n"
  },
  {
    "path": "src/demo/components/SidebarSection.js",
    "content": "import { By } from 'selenium-webdriver';\n\nimport BaseWrapper from './BaseWrapper';\nimport ReminderItem from './ReminderItem';\n\nexport default class SidebarSection extends BaseWrapper {\n    static async get(webdriver, name) {\n        const element = await webdriver.findElement(By.xpath(\n            '//div[contains(@class, \\'sidebar-section\\')]'\n            + '/div[contains(@class, \\'cursor\\')]'\n            + `/div/div[contains(text(), '${name}')]`\n            + '/../../..',\n        ));\n        return new this(webdriver, element);\n    }\n\n    async getItems() {\n        const items = await this.element.findElements(\n            By.xpath(\".//div[contains(@class, 'highlightable')]//div[contains(@class, 'input-line')]\"),\n        );\n        return Promise.all(items.map((item) => item.getText()));\n    }\n\n    async getReminderItems() {\n        const items = await this.element.findElements(By.xpath(\".//div[contains(@class, 'reminder-item')]\"));\n        return items.map((item) => new ReminderItem(this.webdriver, item));\n    }\n}\n"
  },
  {
    "path": "src/demo/components/index.js",
    "content": "import Application from './Application';\n\nexport default Application;\n"
  },
  {
    "path": "src/demo/index.js",
    "content": "/* eslint-disable no-console */\n\nimport { assert } from 'console';\nimport fs from 'fs';\nimport path from 'path';\nimport { Builder } from 'selenium-webdriver';\nimport yargs from 'yargs';\n\nimport runLessons from './lessons';\nimport { ProcessWrapper, StreamIntender } from './process';\n\nconst DEVICE_TO_WINDOW_SPEC = {\n    desktop: { width: 1920, height: 1080 },\n    mobile: { width: 1280, height: 720 },\n};\n\nasync function main(argv) {\n    console.info('Initializing ...');\n\n    if (!argv.verbose) {\n        console.info(`${argv.indent}Note: Child process output hidden! (hint: --verbose)`);\n    }\n\n    const config = JSON.parse(fs.readFileSync(argv.configPath));\n    const dataDir = path.dirname(config.database.storage);\n    try {\n        fs.accessSync(dataDir);\n    } catch (_e) {\n        fs.mkdirSync(dataDir);\n    }\n    console.info(`${argv.indent}Prepared data directory!`);\n\n    const [serverCommand, ...serverArgs] = 'yarn run server -c'.split(' ').concat(argv.configPath);\n    const databaseResetProcess = new ProcessWrapper({\n        command: serverCommand,\n        argv: serverArgs.concat('-a', 'database-reset'),\n        stream: new StreamIntender(process.stdout, `${argv.indent + argv.indent}[database-reset] `),\n        verbose: argv.verbose,\n    });\n    await databaseResetProcess.start();\n    await databaseResetProcess.waitUntilExit();\n    console.info(`${argv.indent}Reset Database!`);\n\n    const serverProcess = new ProcessWrapper({\n        command: serverCommand,\n        argv: serverArgs,\n        stream: new StreamIntender(process.stdout, `${argv.indent + argv.indent}[server] `),\n        verbose: argv.verbose,\n    });\n    await serverProcess.start();\n    await serverProcess.waitUntilOutput('Server running');\n    console.info(`${argv.indent}Test server started!`);\n\n    const webdriver = new Builder().forBrowser('chrome').build();\n    const spec = DEVICE_TO_WINDOW_SPEC[argv.device];\n    assert(spec, `unknown device: ${argv.device}`);\n    await webdriver.manage().window().setRect({\n        width: spec.width, height: spec.height, x: 0, y: 0,\n    });\n    await webdriver.get(`http://${config.server.host}:${config.server.port}`);\n    console.info(`${argv.indent}Webdriver started!`);\n\n    let recordingProcess;\n    if (argv.record) {\n        const rect = await webdriver.manage().window().getRect();\n        recordingProcess = new ProcessWrapper({\n            command: 'ffmpeg',\n            argv: (\n                '-f avfoundation '\n                + '-i 1: ' // ffmpeg -f avfoundation -list_devices true -i \"\"\n                + '-pix_fmt yuv420p '\n                + `-vf crop=${rect.width}:${rect.height}:${rect.x}:${rect.y} `\n                + `-y ${argv.videoPath}`\n            ).split(' '),\n            stream: new StreamIntender(process.stdout, `${argv.indent + argv.indent}[screen-recording] `),\n            verbose: argv.verbose,\n        });\n        await recordingProcess.start();\n        await recordingProcess.waitUntilOutput(argv.videoPath);\n        console.info(`${argv.indent}Screen recording started!`);\n    } else {\n        console.info(`${argv.indent}Skipped screen recording! (hint: --record)`);\n    }\n\n    console.info('Initialized!');\n\n    try {\n        await runLessons(webdriver, argv);\n    } catch (error) {\n        console.error(error);\n    }\n\n    console.info('Terminating ...');\n\n    if (argv.record) {\n        await recordingProcess.stop();\n    }\n    await webdriver.quit();\n    await serverProcess.stop();\n\n    if (argv.record && argv.videoPath.endsWith('.mkv')) {\n        // Directly generating an MP4 file does not work for some reason.\n        console.info(`${argv.indent}Converting to mp4 format ...`);\n        const formatConversionProcess = new ProcessWrapper({\n            command: 'ffmpeg',\n            argv: (\n                `-i ${argv.videoPath} `\n                + '-codec copy '\n                + `-y ${argv.videoPath.replace('mkv', 'mp4')}`\n            ).split(' '),\n            stream: new StreamIntender(process.stdout, `${argv.indent + argv.indent}[format-conversion] `),\n            verbose: argv.verbose,\n        });\n        await formatConversionProcess.start();\n        await formatConversionProcess.waitUntilExit();\n    }\n\n    console.info('Terminated!');\n}\n\nconst { argv } = yargs\n    // General\n    .option('config-path', { alias: 'c', default: './config/demo.glados.json' })\n    .demandOption('config-path')\n    .option('verbose')\n    .option('indent', { default: '\\t' })\n    // Recording\n    .option('record')\n    .option('videoPath', { default: './data/glados.demo.mkv' })\n    .option('device', { default: 'desktop' })\n    // Lessons\n    .option('filter')\n    .option('wait', { default: 0 });\n\nmain(argv).catch((error) => console.error(error));\n"
  },
  {
    "path": "src/demo/lessons/001-events.js",
    "content": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexSection();\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('This is an event.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 1);\n\n        await adder.typeSlowly('Events are used to log what you did throughout the day.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 2);\n    }\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(1);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('Or what you plan to do.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 1);\n\n        const bulletItem = await bulletList.getItem(-1);\n        await bulletItem.performAction('Complete');\n    }\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        const count = await bulletList.getItemCount();\n        await adder.typeSlowly('You can add details to an event.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === count + 1);\n\n        const bulletItem = await bulletList.getItem(count);\n        await bulletItem.perform('Edit');\n        await app.waitUntil(async () => !!(await app.getModalDialog(0)));\n\n        const modalDialog = await app.getModalDialog(0);\n        const detailsInput = await modalDialog.getTextEditor('Details');\n        await detailsInput.typeSlowly('Unlike the event title, ');\n        await detailsInput.typeSlowly('the details section is not limited to one line.');\n        await detailsInput.sendKeys('ENTER');\n        await detailsInput.typeSlowly('This is where you can add a lot more context about what happened.');\n        await modalDialog.performSave();\n\n        await bulletItem.perform('Expand');\n        await bulletItem.perform('Collapse');\n    }\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n        const count = await bulletList.getItemCount();\n\n        await adder.typeSlowly('Some events could be more important than others.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === count + 1);\n\n        const minorItem = await bulletList.getItem(-1);\n        await minorItem.perform('Edit');\n        await app.waitUntil(async () => !!(await app.getModalDialog(0)));\n\n        const modalDialog = await app.getModalDialog(0);\n        const logLevelSelector = await modalDialog.getSelector('Log Level');\n        await logLevelSelector.pickOption('Minor (1)');\n        const detailTextEditor = await modalDialog.getTextEditor('Details');\n        await detailTextEditor.typeSlowly('Minor events are not displayed by default.');\n\n        await modalDialog.performSave();\n        await app.waitUntil(async () => await bulletList.getItemCount() === count);\n\n        const typeaheadSelector = await indexSection.getTypeahead();\n        await typeaheadSelector.typeSlowly('L');\n        const name = 'Log Level: Minor+';\n        await typeaheadSelector.pickSuggestion(name);\n        await app.waitUntil(async () => (await typeaheadSelector.getTokens())[0] === name);\n        await app.waitUntil(async () => await bulletList.getItemCount() === count + 1);\n\n        await adder.typeSlowly('While viewing all events, you can reorder them.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === count + 2);\n\n        const bulletItem = await bulletList.getItem(-1);\n        await bulletItem.move('UP');\n        await bulletItem.move('UP');\n    }\n};\n"
  },
  {
    "path": "src/demo/lessons/002-topics.js",
    "content": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexSection();\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('Let us now create some topics.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 1);\n    }\n\n    if (true) {\n        await app.switchToTab('Manage Topics');\n\n        const bulletList0 = await indexSection.getBulletList(0);\n        await app.performCreateNew(bulletList0);\n        await app.performInputName('Personal Projects');\n        await app.waitUntil(async () => await bulletList0.getItemCount() === 1);\n\n        const bulletItem1 = await bulletList0.getItem(0);\n        await bulletItem1.perform('Expand');\n\n        const bulletList1 = await bulletItem1.getSubList();\n        await app.performCreateNew(bulletList1);\n        await app.performInputName('GLADOS');\n        await app.waitUntil(async () => await bulletList1.getItemCount() === 1);\n\n        await app.performCreateNew(bulletList0);\n        await app.performInputName('People');\n        await app.waitUntil(async () => await bulletList0.getItemCount() === 2);\n\n        const bulletItem2 = await bulletList0.getItem(1);\n        await bulletItem2.perform('Expand');\n\n        const bulletList2 = await bulletItem2.getSubList();\n        await app.performCreateNew(bulletList2);\n        await app.performInputName('Kasturi Karkare');\n        await app.waitUntil(async () => await bulletList2.getItemCount() === 1);\n    }\n\n    if (true) {\n        await app.switchToTab('Manage Events');\n\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('You can reference topics from events.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 2);\n\n        await adder.typeSlowly('Created demo video for @G');\n        await adder.pickSuggestion(0);\n        await adder.typeSlowly('using Selenium.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 3);\n\n        await adder.typeSlowly('Conversation with @K');\n        await adder.pickSuggestion(0);\n        await adder.typeSlowly('about @G');\n        await adder.pickSuggestion(0);\n        await adder.sendKeys('BACK_SPACE');\n        await adder.typeSlowly('.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 4);\n    }\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const count = await bulletList.getItemCount();\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('You can click on a topic to show details on the right side.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === count + 1);\n\n        let topicElement = await app.getTopic('GLADOS', 0);\n        await topicElement.moveToAndClick();\n        await app.waitUntil(async () => app.isDetailsSectionActive());\n\n        const detailsSection = await app.getDetailsSection(0);\n        await detailsSection.typeSlowly('You can add details about a particular topic.');\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.sendKeys('ENTER');\n\n        await detailsSection.typeSlowly('You can search for all events referencing this topic,');\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.typeSlowly('by clicking on the magnifying glass icon above!');\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.sendKeys('ENTER');\n\n        await detailsSection.perform('Search');\n        const typeahead = await indexSection.getTypeahead();\n        await app.waitUntil(async () => (await typeahead.getTokens()).length === 1);\n\n        topicElement = await app.getTopic('GLADOS', 0);\n        await topicElement.moveTo();\n        topicElement = await app.getTopic('GLADOS', 1);\n        await topicElement.moveTo();\n\n        await typeahead.removeToken('GLADOS');\n        await app.waitUntil(async () => (await typeahead.getTokens()).length === 0);\n\n        await detailsSection.typeSlowly('You can mark this event as a favorite using the heart icon.');\n        await detailsSection.sendKeys('ENTER');\n\n        await detailsSection.perform('Favorite');\n        const favoriteTopics = await app.getSidebarSection('Favorite Topics');\n        await app.waitUntil(async () => (await favoriteTopics.getItems()).length === 1);\n\n        await detailsSection.typeSlowly('It now appears on the right sidebar.');\n        await detailsSection.sendKeys('ENTER');\n\n        await detailsSection.perform('Close');\n        await app.waitUntil(async () => !(await detailsSection.isActive()));\n\n        topicElement = await app.getTopic('GLADOS', -1);\n        topicElement.moveTo();\n        topicElement.click();\n        await app.waitUntil(async () => detailsSection.isActive());\n    }\n};\n"
  },
  {
    "path": "src/demo/lessons/003-structures.js",
    "content": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexSection();\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('You can add structured data to your events.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 1);\n    }\n\n    if (true) {\n        await app.switchToTab('Manage Structures');\n\n        const bulletList0 = await indexSection.getBulletList(0);\n        await app.performCreateNew(bulletList0);\n        await app.performInputName('Exercise');\n\n        const bulletItem1 = await bulletList0.getItem(0);\n        await bulletItem1.perform('Expand');\n        await app.waitUntil(async () => !!(await bulletItem1.getSubList()));\n\n        const bulletList1 = await bulletItem1.getSubList();\n        const modalDialog = await app.performCreateNew(bulletList1);\n\n        const nameInput = await modalDialog.getTextInput('Name');\n        await nameInput.typeSlowly('Running');\n\n        const key1 = await modalDialog.addLogStructureKey();\n        const key1type = await key1.getTypeSelector();\n        await key1type.pickOption('Number');\n        const key1name = await key1.getNameInput();\n        await key1name.typeSlowly('Distance (km)');\n\n        const templateInput = await modalDialog.getTextEditor('Event Title Template');\n        await templateInput.typeSlowly(': @D');\n        await templateInput.pickSuggestion(0);\n        await templateInput.typeSlowly('km / ');\n\n        const key2 = await modalDialog.addLogStructureKey();\n        const key2type = await key2.getTypeSelector();\n        await key2type.pickOption('Number');\n        const key2name = await key2.getNameInput();\n        await key2name.typeSlowly('Time (minutes)');\n\n        await templateInput.typeSlowly('@T');\n        await templateInput.pickSuggestion(0);\n        await templateInput.typeSlowly('minutes');\n\n        await modalDialog.performSave();\n    }\n\n    if (true) {\n        await app.switchToTab('Manage Events');\n\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('@R');\n        await adder.pickSuggestion(0);\n        await app.waitUntil(async () => !!(await app.getModalDialog(0)));\n\n        const modalDialog = await app.getModalDialog(0);\n        const distanceInput = await modalDialog.getTypeahead('Distance (km)');\n        await distanceInput.typeSlowly('10');\n        const timeInput = await modalDialog.getTypeahead('Time (minutes)');\n        await timeInput.typeSlowly('60');\n\n        await modalDialog.performSave();\n    }\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('You can derive additional information from structured data.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 3);\n\n        const topicElement = await app.getTopic('Running', 0);\n        await topicElement.moveToAndClick();\n        await app.waitUntil(async () => app.isDetailsSectionActive());\n\n        const detailsSection = await app.getDetailsSection(0);\n        await detailsSection.perform('Edit');\n\n        const modalDialog = await app.getModalDialog(0);\n\n        const key3 = await modalDialog.addLogStructureKey();\n        const key3type = await key3.getTypeSelector();\n        await key3type.pickOption('Number');\n        const key3name = await key3.getNameInput();\n        await key3name.typeSlowly('Speed (kmph)');\n        const key3template = await key3.getTemplateInput();\n        await key3template.typeSlowly('{( @D');\n        await key3template.pickSuggestion(0);\n        await key3template.typeSlowly(' * 60 / @T');\n        await key3template.pickSuggestion(0);\n        await key3template.typeSlowly(').toFixed(2)}');\n\n        const templateInput = await modalDialog.getTextEditor('Event Title Template');\n        await templateInput.sendKeys('BACK_SPACE');\n        await templateInput.typeSlowly(' (@S');\n        await templateInput.pickSuggestion(0);\n        await templateInput.typeSlowly(' kmph)');\n\n        await modalDialog.performSave();\n    }\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const bulletItem = await bulletList.getItem(1);\n\n        await bulletItem.perform('Edit');\n        await app.waitUntil(async () => !!(await app.getModalDialog(0)));\n\n        const modalDialog = await app.getModalDialog(0);\n        const timeInput = await modalDialog.getTypeahead('Time (minutes)');\n        await timeInput.sendKeys('BACK_SPACE', 'BACK_SPACE');\n        await timeInput.typeSlowly('50');\n\n        await modalDialog.performSave();\n    }\n};\n"
  },
  {
    "path": "src/demo/lessons/004-reminders.js",
    "content": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexSection();\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('You can add \"structure\" to your day using reminders.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 1);\n    }\n\n    if (true) {\n        await app.switchToTab('Manage Structures');\n\n        const bulletList0 = await indexSection.getBulletList(0);\n        await app.performCreateNew(bulletList0);\n        await app.performInputName('Daily Routine');\n\n        const bulletItem1 = await bulletList0.getItem(0);\n        await bulletItem1.perform('Expand');\n        await app.waitUntil(async () => !!(await bulletItem1.getSubList()));\n\n        const bulletList1 = await bulletItem1.getSubList();\n\n        await app.performCreateNew(bulletList1).then(async (modalDialog) => {\n            const nameInput = await modalDialog.getTextInput('Name');\n            await nameInput.typeSlowly('Woke up');\n\n            const templateInput = await modalDialog.getTextEditor('Event Title Template');\n            await templateInput.typeSlowly(' at ');\n\n            const logLevelSelector = await modalDialog.getSelector('Log Level');\n            await logLevelSelector.pickOption('Minor (1)');\n\n            const key1 = await modalDialog.addLogStructureKey();\n            const key1type = await key1.getTypeSelector();\n            await key1type.pickOption('Time');\n            const key1name = await key1.getNameInput();\n            await key1name.typeSlowly('Time');\n\n            await templateInput.typeSlowly('@T');\n            await templateInput.pickSuggestion(0);\n            await templateInput.sendKeys('BACK_SPACE');\n\n            const isPeriodicSelector = await modalDialog.getSelector('Is Periodic?');\n            await isPeriodicSelector.pickOption('Yes');\n\n            await modalDialog.performSave();\n        });\n\n        await app.performCreateNew(bulletList1).then(async (modalDialog) => {\n            const nameInput = await modalDialog.getTextInput('Name');\n            await nameInput.typeSlowly('Made Bed');\n\n            const isPeriodicSelector = await modalDialog.getSelector('Is Periodic?');\n            await isPeriodicSelector.pickOption('Yes');\n\n            const reminderTextInput = await modalDialog.getTextInput('Reminder Text');\n            await reminderTextInput.typeSlowly('Make Bed');\n\n            await modalDialog.performSave();\n        });\n    }\n\n    if (true) {\n        await app.switchToTab('Manage Events');\n\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('Reminders appear on the left sidebar.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 2);\n\n        const sidebarSection = await app.getSidebarSection('Daily Routine');\n\n        let reminderItems = await sidebarSection.getReminderItems();\n        await reminderItems[0].getCheckbox().then(async (checkbox) => {\n            await checkbox.moveTo();\n            await app.wait();\n            await checkbox.click();\n\n            const modalDialog = await app.getModalDialog(0);\n            const timeInput = await modalDialog.getTypeahead('Time');\n            await timeInput.typeSlowly('07:00');\n            await modalDialog.performSave();\n        });\n\n        reminderItems = await sidebarSection.getReminderItems();\n        await reminderItems[0].pickMenuItem('Mark as Complete');\n    }\n};\n"
  },
  {
    "path": "src/demo/lessons/005-graphs.js",
    "content": "/* eslint-disable no-constant-condition */\n\nexport default async (app) => {\n    const indexSection = await app.getIndexSection();\n\n    if (true) {\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('You can view graphs of your events.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 1);\n\n        await adder.typeSlowly('Let us create a number of mock events to demonstrate that.');\n        await adder.sendKeys('ENTER');\n        await app.waitUntil(async () => await bulletList.getItemCount() === 2);\n    }\n\n    if (true) {\n        await app.switchToTab('Manage Structures');\n\n        const bulletList0 = await indexSection.getBulletList(0);\n        await app.performCreateNew(bulletList0);\n        await app.performInputName('Exercise');\n\n        const bulletItem1 = await bulletList0.getItem(0);\n        await bulletItem1.perform('Expand');\n        await app.waitUntil(async () => !!(await bulletItem1.getSubList()));\n\n        const bulletList1 = await bulletItem1.getSubList();\n\n        await app.performCreateNew(bulletList1).then(async (modalDialog) => {\n            const nameInput = await modalDialog.getTextInput('Name');\n            await nameInput.typeSlowly('Push Ups');\n\n            const templateInput = await modalDialog.getTextEditor('Event Title Template');\n            await templateInput.typeSlowly(': ');\n\n            const key1 = await modalDialog.addLogStructureKey();\n            const key1type = await key1.getTypeSelector();\n            await key1type.pickOption('Integer');\n            const key1name = await key1.getNameInput();\n            await key1name.typeSlowly('Count');\n\n            await templateInput.typeSlowly('@C');\n            await templateInput.pickSuggestion(0);\n            await templateInput.sendKeys('BACK_SPACE');\n\n            await modalDialog.performSave();\n        });\n    }\n\n    let logEventTemplate;\n    if (true) {\n        await app.switchToTab('Manage Events');\n\n        const bulletList = await indexSection.getBulletList(0);\n        const adder = await bulletList.getAdder();\n\n        await adder.typeSlowly('@P');\n        await adder.pickSuggestion(0);\n        await app.waitUntil(async () => !!(await app.getModalDialog(0)));\n\n        await app.getModalDialog(0).then(async (modalDialog) => {\n            const countInput = await modalDialog.getTypeahead('Count');\n            await countInput.typeSlowly('50');\n            await modalDialog.performSave();\n        });\n\n        const typeaheadSelector = await indexSection.getTypeahead();\n        await typeaheadSelector.typeSlowly('P');\n        await typeaheadSelector.pickSuggestion('Push Ups');\n        await app.waitUntil(async () => (await typeaheadSelector.getTokens())[0] === 'Push Ups');\n    }\n\n    if (true) {\n        const topicElement = await app.getTopic('Push Ups', 0);\n        await topicElement.moveToAndClick();\n        await app.waitUntil(async () => app.isDetailsSectionActive());\n\n        const detailsSection = await app.getDetailsSection(0);\n        await detailsSection.typeSlowly('Using \"Debug Info\" to make RPCs to create similar events.');\n        await detailsSection.sendKeys('ENTER');\n\n        const bulletList = await indexSection.getBulletList(0);\n        const bulletItem = await bulletList.getItem(0);\n        await bulletItem.click();\n        await bulletItem.performAction('Debug Info');\n        await app.waitUntil(async () => !!(await app.getModalDialog(0)));\n\n        await app.getModalDialog(0).then(async (modalDialog) => {\n            logEventTemplate = JSON.parse(await modalDialog.getDebugInfo());\n            await modalDialog.performClose();\n        });\n\n        const timestamp = new Date(logEventTemplate.date).valueOf();\n        const msInDay = 24 * 60 * 60 * 1000;\n        const totalEvents = 30; // to avoid exceeding page size.\n        const initialValue = 10;\n        const finalValue = parseInt(logEventTemplate.logStructure.eventKeys[0].value, 10);\n        let count = await bulletList.getItemCount();\n        for (let index = 1; index < totalEvents; index += 1) {\n            count += 1;\n            logEventTemplate.__id__ = -count;\n\n            const newDate = new Date(timestamp - msInDay * index);\n            // eslint-disable-next-line prefer-destructuring\n            logEventTemplate.date = newDate.toISOString().split('T')[0];\n            // Skip weekends to avoid a straight line.\n            if ([0, 6].includes(newDate.getDay())) {\n                count -= 1;\n                // eslint-disable-next-line no-continue\n                continue;\n            }\n            // Extrapolating events using a linear equation.\n            // Options explored using https://www.desmos.com/calculator\n            let value = initialValue + Math.floor(\n                ((finalValue - initialValue) * (totalEvents - index - 1)) / totalEvents,\n            );\n            // Add some randomness.\n            if (index < totalEvents - 1) {\n                const variation = Math.ceil(10 / 4);\n                value += Math.ceil(Math.random() * variation) - Math.ceil(variation / 2);\n            }\n            logEventTemplate.logStructure.eventKeys[0].value = value.toString();\n            // eslint-disable-next-line no-await-in-loop\n            await app.webdriver.executeScript(`window.api.send('log-event-upsert', ${JSON.stringify(logEventTemplate)})`);\n            // eslint-disable-next-line no-await-in-loop, no-loop-func\n            await app.waitUntil(async () => await bulletList.getItemCount() === count);\n        }\n\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.typeSlowly('Note that weekends were skipped while creating the mock events.');\n        await detailsSection.sendKeys('ENTER');\n    }\n\n    if (true) {\n        await app.switchToTab('Explore Graphs');\n\n        await app.wait(2000);\n\n        const typeaheadSelector = await indexSection.getTypeahead();\n        await typeaheadSelector.typeSlowly('Gr');\n        await typeaheadSelector.pickSuggestion('Granularity: Day');\n\n        await app.wait(1000);\n\n        const detailsSection = await app.getDetailsSection(0);\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.typeSlowly('The \"Event Count\" graph is an indicator of your consistency.');\n        await detailsSection.sendKeys('ENTER');\n\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.typeSlowly('Additional graphs are generated for each numerical key of your structure,');\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.typeSlowly('and can help see patterns in those values.');\n        await detailsSection.sendKeys('ENTER');\n\n        await detailsSection.sendKeys('ENTER');\n        await detailsSection.typeSlowly('Let us change the layout for better visibility.');\n        await detailsSection.sendKeys('ENTER');\n\n        const linkElement = await app.getLink('Left');\n        await linkElement.moveToAndClick();\n\n        await app.wait(1000);\n\n        await app.scrollToBottom('scrollable-section', 1);\n\n        await app.wait(1000);\n    }\n};\n"
  },
  {
    "path": "src/demo/lessons.js",
    "content": "/* eslint-disable no-console */\n\nimport { asyncSequence } from '../common/AsyncUtils';\nimport Application from './components';\n\nconst lessonsContext = require.context('./lessons', false, /\\.js$/);\n\nexport default async (webdriver, argv) => {\n    if (!argv.filter) {\n        console.info(`${argv.indent}Note: Running all lessons! (hint: --filter)`);\n    }\n\n    const resetUrl = await webdriver.getCurrentUrl();\n    const app = new Application(webdriver);\n    const lessonNames = lessonsContext.keys()\n        .filter((name) => {\n            if (!argv.filter) {\n                return true;\n            }\n            // Remove the \"./\" prefix and \".js\" suffix.\n            return name.slice(2, -3).includes(argv.filter);\n        });\n\n    await asyncSequence(lessonNames, async (name) => {\n        const { default: lessonMethod } = lessonsContext(name);\n        console.info(`${argv.indent}Lesson: ${name}`);\n        try {\n            await app.clearDatabase();\n            await webdriver.get(resetUrl);\n            await lessonMethod(app);\n            await app.wait(1000); // 1 sec\n        } catch (error) {\n            console.error(error);\n            await app.wait(argv.wait * 1000); // Use this time to debug.\n            throw error;\n        }\n    });\n    await app.wait(argv.wait * 1000); // Use this time to debug.\n};\n"
  },
  {
    "path": "src/demo/process.js",
    "content": "/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\nimport childProcess from 'child_process';\n\nclass ProcessWrapper {\n    constructor(args) {\n        assert(args.command && Array.isArray(args.argv) && args.stream);\n        this.process = null;\n        this._lastDataTimestamp = null; // Set from _onData, for waitUntilPause\n        this._outputBuffer = ''; // Set from _onData, for waitUntilOutput\n        this._checkForOutput = null; // Set from waitUntilOutput\n        this._pauseInterval = null; // Set from waitUntilPause\n        this._args = args;\n    }\n\n    async start() {\n        const {\n            command, argv, stream, verbose,\n        } = this._args;\n        this._lastDataTimestamp = Date.now();\n        if (verbose) {\n            stream.write(`$ ${[command, ...argv].join(' ')}\\n`);\n        }\n        this.process = childProcess.spawn(command, argv);\n        this.process.stdout.on('data', (data) => this._onData(data));\n        this.process.stderr.on('data', (data) => this._onData(data));\n        this.process.on('exit', () => {\n            this.process.stdin.end();\n            this.process.stdout.destroy();\n            this.process.stderr.destroy();\n        });\n    }\n\n    _onData(data) {\n        const { stream, verbose } = this._args;\n        this._lastDataTimestamp = Date.now();\n        data = data.toString();\n        if (verbose) {\n            stream.write(data);\n        }\n        this._outputBuffer += data;\n        if (this._checkForOutput) {\n            this._checkForOutput();\n        }\n    }\n\n    async waitUntilOutput(text) {\n        assert(!!this.process);\n        if (this._outputBuffer.includes(text)) {\n            return Promise.resolve();\n        }\n        return new Promise((resolve) => {\n            this._checkForOutput = () => {\n                if (this._outputBuffer.includes(text)) {\n                    this._checkForOutput = null;\n                    resolve();\n                }\n            };\n        });\n    }\n\n    async waitUntilPause(duration) {\n        assert(!!this.process);\n        return new Promise((resolve) => {\n            this._pauseInterval = setInterval(() => {\n                const delta = Date.now() - this._lastDataTimestamp;\n                if (delta > duration) {\n                    clearInterval(this._pauseInterval);\n                    this._pauseInterval = null;\n                    resolve();\n                }\n            }, 100);\n        });\n    }\n\n    async waitUntilExit() {\n        assert(!!this.process);\n        return new Promise((resolve) => {\n            this.process.on('close', resolve);\n        });\n    }\n\n    async stop() {\n        assert(!!this.process);\n        return new Promise((resolve) => {\n            this.process.on('close', resolve);\n            this.process.kill();\n        });\n    }\n}\n\nclass StreamIntender {\n    constructor(stream, prefix) {\n        this._stream = stream;\n        this._prefix = prefix;\n        this._pending = true;\n    }\n\n    write(data) {\n        let prefix = '';\n        if (this._pending) {\n            prefix += this._prefix;\n            this._pending = false;\n        }\n        data.split('\\n').forEach((line, index, lines) => {\n            if (index < lines.length - 1) {\n                this._stream.write(`${prefix + line}\\n`);\n                prefix = this._prefix;\n            } else if (line) {\n                this._stream.write(prefix + line);\n            } else {\n                this._pending = true;\n            }\n        });\n    }\n}\n\nexport { ProcessWrapper, StreamIntender };\n"
  },
  {
    "path": "src/plugins/README.md",
    "content": "### Expectations\n\n* File names ending with `actions.js` are supposed to export an object, whose keys are \"action names\" and values are the corresponding implementations, which are new RPCs that the server will now support.\n* File names ending with `client.js` are supposed to export a class that extends the `[PluginClient](../client/Common/Plugins.js)` interface, which describes how to augment the UI.\n\n### Activation\n\n* In your `config.json` file, you will need to provide a list of regexes that match the plugin paths that you want to activate:\n\n```\n{\n    ...\n    \"plugins\": [\n        \"kaustubh/.*\"\n    ]\n}\n```\n"
  },
  {
    "path": "src/plugins/kaustubh/custom.actions.js",
    "content": "/* eslint-disable func-names */\n/* eslint-disable camelcase */\n/* eslint-disable max-len */\n/* eslint-disable no-console */\n/* eslint-disable no-constant-condition */\n\nimport assert from 'assert';\n\nimport { asyncSequence } from '../../common/AsyncUtils';\nimport { getPartialItem } from '../../common/data_types';\nimport RichTextUtils from '../../common/RichTextUtils';\n\nconst ActionsRegistry = {};\n\nActionsRegistry['check-consistency'] = async function () {\n    const results = [];\n    // These items only contain the __type__, __id__ & name.\n    const logTopicItems = await this.invoke.call(this, 'log-topic-typeahead', { query: '' });\n\n    if (false) {\n        // Update logTopics using latest topic-names\n        const logTopics = await this.invoke.call(this, 'log-topic-list');\n        await asyncSequence(logTopics, async (logTopic) => {\n            try {\n                logTopic.details = RichTextUtils.updateDraftContent(logTopic.details, logTopicItems);\n                await this.invoke.call(this, 'log-topic-upsert', logTopic);\n            } catch (error) {\n                results.push([logTopic, error.toString()]);\n            }\n        });\n    }\n\n    if (false) {\n        // Update logStructures using latest topic-names\n        const logStructures = await this.invoke.call(this, 'log-structure-list');\n        await asyncSequence(logStructures, async (logStructure) => {\n            try {\n                logStructure.eventTitleTemplate = RichTextUtils.updateDraftContent(logStructure.eventTitleTemplate, logTopicItems);\n                // TODO: Update topics in keys too.\n                await this.invoke.call(this, 'log-structure-upsert', logStructure);\n            } catch (error) {\n                results.push([logStructure, error.toString()]);\n            }\n        });\n    }\n\n    if (false) {\n        // Update logEvents using latest topic-names & structure-title-template.\n        const logEvents = await this.invoke.call(this, 'log-event-list');\n        await asyncSequence(logEvents, async (logEvent) => {\n            try {\n                logEvent.title = RichTextUtils.updateDraftContent(logEvent.title, logTopicItems);\n                logEvent.details = RichTextUtils.updateDraftContent(logEvent.details, logTopicItems);\n                await this.invoke.call(this, 'log-event-upsert', logEvent);\n            } catch (error) {\n                results.push([logEvent, error.toString()]);\n            }\n        });\n    }\n\n    return results;\n};\n\nActionsRegistry['fix-birthdays-anniversaries'] = async function () {\n    // Update structures so that they each have similar behavior.\n\n    const GROUP_ID_TO_NAME = {\n        9: 'Birthdays',\n        13: 'Anniversaries',\n    };\n    const logStructureGroups = await this.invoke.call(\n        this,\n        'log-structure-group-list',\n        { where: { __id__: Object.keys(GROUP_ID_TO_NAME) } },\n    );\n    return Promise.all(\n        logStructureGroups.map(async (logStructureGroup) => {\n            assert(\n                GROUP_ID_TO_NAME[logStructureGroup.__id__] === logStructureGroup.name,\n                logStructureGroup.name,\n            );\n            const logStructures = await this.invoke.call(\n                this,\n                'log-structure-list',\n                { where: { logStructureGroup } },\n            );\n            return Promise.all(\n                logStructures.map(async (logStructure) => {\n                    const nameRegexResult = logStructure.name.match(/^(\\d{2}-\\d{2})\\w?$/);\n                    assert(nameRegexResult, logStructure.name);\n                    const expectedValues = {\n                        isPeriodic: true,\n                        frequency: 'yearly',\n                        frequencyArgs: nameRegexResult[1],\n                        reminderText:\n                            RichTextUtils.extractPlainText(logStructure.eventTitleTemplate),\n                        warningDays: 2,\n                    };\n                    let needsUpdate = false;\n                    Object.keys(expectedValues).forEach((key) => {\n                        if (logStructure[key] !== expectedValues[key]) {\n                            logStructure[key] = expectedValues[key];\n                            needsUpdate = true;\n                        }\n                    });\n                    if (!needsUpdate) {\n                        return logStructure;\n                    }\n                    return this.invoke.call(\n                        this,\n                        'log-structure-upsert',\n                        logStructure,\n                    );\n                }),\n            );\n        }),\n    );\n};\n\nActionsRegistry['update-television-events'] = async function (data) {\n    // Used to change \"Television\" structure events\n    // to use a topic as a log-key, instead of a string.\n\n    const structure_id = data.log_structures\n        .filter((log_structure) => log_structure.name === 'Television')[0].id;\n    const parent_topic_id = data.log_topics\n        .filter((log_topic) => log_topic.name === 'Television Series')[0].id;\n    const value_index = 0;\n\n    data.log_structures.forEach((log_structure) => {\n        if (log_structure.id === structure_id) {\n            const keys = JSON.parse(log_structure.keys);\n            keys[0].type = 'log_topic';\n            keys[0].is_optional = false;\n            keys[0].parent_topic_id = parent_topic_id;\n            log_structure.keys = JSON.stringify(keys);\n            data.log_structures_to_log_topics.push({\n                source_structure_id: log_structure.id,\n                target_topic_id: parent_topic_id,\n            });\n        }\n    });\n    let maxTopicId = Math.max(...data.log_topics.map((log_topic) => log_topic.id));\n    const nameToTopicId = {};\n    data.log_topics.forEach((log_topic) => {\n        if (log_topic.parent_topic_id === parent_topic_id) {\n            nameToTopicId[log_topic.name] = log_topic.id;\n        }\n    });\n    data.log_events.forEach((log_event) => {\n        if (log_event.structure_id === structure_id) {\n            const values = JSON.parse(log_event.structure_values);\n            const series_name = values[value_index];\n            if (!nameToTopicId[series_name]) {\n                maxTopicId += 1;\n                const new_topic_id = maxTopicId;\n                data.log_topics.push({\n                    id: new_topic_id,\n                    mode_id: 1,\n                    parent_topic_id,\n                    ordering_index: 0,\n                    name: series_name,\n                    details: '',\n                    child_count: 0,\n                    is_favorite: 0,\n                    is_deprecated: 0,\n                });\n                nameToTopicId[series_name] = new_topic_id;\n            }\n            const topic_id = nameToTopicId[series_name];\n            values[value_index] = {\n                __type__: 'log-topic',\n                __id__: topic_id,\n                name: series_name,\n            };\n            log_event.structure_values = JSON.stringify(values);\n            data.log_events_to_log_topics.push({\n                source_event_id: log_event.id,\n                target_topic_id: topic_id,\n            });\n        }\n    });\n    if (false) {\n        const name_to_count = {};\n        data.log_topics.forEach((log_topic) => {\n            if (!(log_topic.name in name_to_count)) {\n                name_to_count[log_topic.name] = 0;\n            }\n            name_to_count[log_topic.name] += 1;\n        });\n        const multiple_topics = Object.entries(name_to_count).filter((kvp) => kvp[1] > 1);\n        if (multiple_topics.length) console.info(multiple_topics);\n    }\n    const validate = async () => {\n        const logStructure = await this.invoke.call(this, 'log-structure-load', { __id__: structure_id });\n        await this.invoke.call(this, 'log-structure-upsert', logStructure);\n    };\n    return { data, validate };\n};\n\nActionsRegistry['update-xyz-events'] = async function (data) {\n    const log_structure = data.log_structures.filter((item) => item.name === 'xyz')[0];\n\n    const keys = JSON.parse(log_structure.keys);\n    keys.splice(2, 1, ...[\n        {\n            name: 'xyz',\n            type: 'string',\n            is_optional: false,\n            template: null,\n            parent_topic_id: null,\n        },\n        {\n            name: 'xyz',\n            type: 'string',\n            is_optional: false,\n            template: null,\n            parent_topic_id: null,\n        },\n    ]);\n    log_structure.keys = JSON.stringify(keys);\n\n    const mapping = {};\n    data.log_events.forEach((log_event) => {\n        if (log_event.structure_id === log_structure.id) {\n            const values = JSON.parse(log_event.structure_values);\n            const new_status_values = mapping[values[2]];\n            if (!new_status_values) {\n                throw log_event;\n            }\n            values.splice(2, 1, ...new_status_values);\n            log_event.structure_values = JSON.stringify(values);\n        }\n    });\n    return { data };\n};\n\nActionsRegistry['add-structure-to-events'] = async function () {\n    // Used to add \"Project Work\" structure to events with the \"GLADOS\" topic.\n    const logTopic = await this.invoke.call(this, 'log-topic-load', { __id__: 4 });\n    const logStructure = await this.invoke.call(this, 'log-structure-load', { __id__: 120 });\n    const logEvents = await this.invoke.call(\n        this,\n        'log-event-list',\n        { where: { logTopics: [logTopic], logStructure: null } },\n    );\n\n    const prefix = `${logTopic.name}: `;\n    await Promise.all(logEvents.map(async (logEvent) => {\n        const oldTitleText = RichTextUtils.extractPlainText(logEvent.title);\n        if (logEvent.logStructure || !oldTitleText.startsWith(prefix)) {\n            return;\n        }\n        logEvent.logStructure = {\n            ...logStructure,\n            eventKeys: logStructure.eventKeys.map((logKey) => ({ ...logKey })),\n        };\n        logEvent.logStructure.eventKeys[0].value = getPartialItem(logTopic);\n        logEvent.logStructure.eventKeys[1].value = RichTextUtils.removePrefixFromDraftContext(\n            logEvent.title,\n            prefix,\n        );\n        // Warning! May need to disable this.database.setEdges in LogEvent.save() to avoid timeout.\n        logEvent = await this.invoke.call(this, 'log-event-upsert', logEvent);\n        const newTitleText = RichTextUtils.extractPlainText(logEvent.title);\n        console.info('Old:', oldTitleText);\n        console.info('New:', newTitleText);\n    }));\n};\n\nActionsRegistry['convert-structure-to-topics'] = async function (data) {\n    const collections_container_topic_id = 458;\n    const original_structure_id = 87;\n\n    const original_structure = data.log_structures.find(\n        (log_structure) => log_structure.id === original_structure_id,\n    );\n\n    // Create replacement topic.\n    let last_topic_id = Math.max(...data.log_topics.map((log_topic) => log_topic.id));\n    last_topic_id += 1;\n    const collection_topic_id = last_topic_id;\n    data.log_topics.push({\n        id: last_topic_id,\n        parent_topic_id: collections_container_topic_id,\n        ordering_index: 24,\n        name: `${original_structure.name}s`,\n        details: '',\n        child_count: 0,\n        is_favorite: 0,\n        is_deprecated: 0,\n        child_keys: original_structure.event_keys,\n        parent_values: null,\n    });\n\n    // Modify existing structure to point to topic.\n    original_structure.event_keys = JSON.stringify([\n        { name: 'Name', type: 'log_topic', parent_topic_id: collection_topic_id },\n    ]);\n\n    // Update all events to use topic.\n    const nameToTopicId = {};\n    data.log_events.forEach((log_event) => {\n        if (log_event.structure_id === original_structure_id) {\n            const topicName = JSON.parse(log_event.structure_values)[0];\n            let topic_id;\n            if (topicName in nameToTopicId) {\n                topic_id = nameToTopicId[topicName];\n            } else {\n                last_topic_id += 1;\n                topic_id = last_topic_id;\n                data.log_topics.push({\n                    id: last_topic_id,\n                    parent_topic_id: collection_topic_id,\n                    ordering_index: 0,\n                    name: topicName,\n                    details: log_event.details,\n                    child_count: 0,\n                    is_favorite: 0,\n                    is_deprecated: 0,\n                    child_keys: null,\n                    parent_values: log_event.structure_values,\n                });\n                log_event.details = '';\n                nameToTopicId[topicName] = topic_id;\n            }\n            log_event.structure_values = JSON.stringify([\n                {\n                    __type__: 'log-topic',\n                    __id__: topic_id,\n                    name: topicName,\n                },\n            ]);\n        }\n    });\n\n    const nameToCount = {};\n    data.log_topics.forEach((log_topic) => {\n        if (log_topic.name in nameToCount) {\n            nameToCount[log_topic.name] += 1;\n        } else {\n            nameToCount[log_topic.name] = 1;\n        }\n    });\n    console.info(Object.entries(nameToCount).filter(([key, value]) => value > 1));\n\n    return { data };\n};\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/plugins/kaustubh/long_term_goals/LongTermGoalGraph.js",
    "content": "import {\n    addDays, compareAsc, differenceInDays,\n} from 'date-fns';\nimport React from 'react';\n\nimport { DateContext } from '../../../client/Common';\nimport { getGraphData, Granularity, GraphLineChart } from '../../../client/Graphs';\nimport PropTypes from '../../../client/prop-types';\nimport DateUtils from '../../../common/DateUtils';\n\nconst CURRENT_KEY = '__current__';\nconst TARGET_KEY = '__target__';\n\nfunction CustomTooltip({ active, label, payload }) {\n    if (active && payload && payload.length) {\n        const output = [];\n        output.push(`Date: ${label}`);\n        payload.forEach((item) => {\n            output.push(`${item.name}: ${item.payload[item.dataKey]}`);\n        });\n        const { logEventTitles } = payload[0].payload;\n        if (logEventTitles.length) {\n            output.push('', ...logEventTitles);\n        }\n        return (\n            <div className=\"graph-tooltip\">\n                {output.map((line) => `${line}\\n`).join('')}\n            </div>\n        );\n    }\n    return null;\n}\n\nCustomTooltip.propTypes = {\n    active: PropTypes.bool,\n    label: PropTypes.string,\n    // eslint-disable-next-line react/forbid-prop-types\n    payload: PropTypes.any,\n};\n\nclass LongTermGoalGraph extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = { graphData: null };\n    }\n\n    componentDidMount() {\n        this.fetchData();\n    }\n\n    async fetchData() {\n        this.setState({ graphData: null });\n        const { goal } = this.props;\n        const granularity = Granularity.DAY;\n\n        // Standard Graph Calculations\n        const dateRange = { ...goal.dateRange, endDate: this.props.todayDate };\n        const where = { date: dateRange, logStructure: goal.logStructure };\n        const logEvents = await window.api.send('log-event-list', { where });\n        const graphData = getGraphData(\n            goal.logStructure,\n            logEvents,\n            goal.dateRange,\n            granularity,\n        );\n\n        // Replace lines.\n        const selectedLine = graphData.lines.find((line) => line.name === goal.keyLabel);\n        graphData.lines = [\n            { name: goal.newLabel, dataKey: CURRENT_KEY },\n            { name: 'Target', dataKey: TARGET_KEY, color: 'var(--link-color)' },\n        ];\n\n        // Limit samples to today.\n        graphData.samples = graphData.samples.filter((sample) => {\n            const sampleDate = DateUtils.getDate(sample.label);\n            return compareAsc(sampleDate, this.props.todayDate) <= 0;\n        });\n\n        // Compute progress and prorated target.\n        const startDate = DateUtils.getDate(goal.dateRange.startDate);\n        const endDate = DateUtils.getDate(goal.dateRange.endDate);\n        const totalDays = differenceInDays(endDate, startDate) + 1;\n        let partialSum = 0;\n        graphData.samples.forEach((sample) => {\n            const sampleDate = DateUtils.getDate(sample.label);\n            let nextSampleDate = addDays(sampleDate, 1);\n            const getGroupLabel = Granularity[granularity].getLabel;\n            while (getGroupLabel(sampleDate) === getGroupLabel(nextSampleDate)) {\n                nextSampleDate = addDays(nextSampleDate, 1);\n            }\n            let dayCount = differenceInDays(nextSampleDate, startDate);\n            dayCount = Math.max(dayCount, 0);\n            dayCount = Math.min(dayCount, totalDays);\n            sample[TARGET_KEY] = (parseFloat(goal.target) * (dayCount / totalDays)).toFixed(2);\n            if (compareAsc(sampleDate, this.props.todayDate) <= 0) {\n                partialSum += sample[selectedLine.valuesKey]\n                    .reduce((result, value) => (result + value), 0);\n                sample[CURRENT_KEY] = partialSum;\n                // Only the last color assigned is applicable.\n                graphData.lines[0].color = sample[CURRENT_KEY] >= sample[TARGET_KEY] ? 'var(--topic-color)' : 'var(--warning-color)';\n            }\n        });\n\n        // Additional Props\n        graphData.tooltip = CustomTooltip;\n        this.setState({ graphData });\n    }\n\n    render() {\n        const { graphData } = this.state;\n        return graphData ? <GraphLineChart {...graphData} /> : null;\n    }\n}\n\nLongTermGoalGraph.propTypes = {\n    todayDate: PropTypes.instanceOf(Date).isRequired,\n    goal: PropTypes.shape({\n        logStructure: PropTypes.Custom.Item.isRequired,\n        keyLabel: PropTypes.string.isRequired,\n        newLabel: PropTypes.string.isRequired,\n        dateRange: PropTypes.Custom.DateRange.isRequired,\n        target: PropTypes.string.isRequired,\n    }).isRequired,\n};\n\nexport default DateContext.Wrapper(LongTermGoalGraph);\n"
  },
  {
    "path": "src/plugins/kaustubh/long_term_goals/LongTermGoalsSettings.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport {\n    DateRangePicker, LeftRight, SortableList, TextInput, TypeaheadOptions, TypeaheadSelector,\n} from '../../../client/Common';\nimport { getNextID } from '../../../common/data_types';\n\nfunction addNewItem(items) {\n    const item = {\n        __id__: getNextID(items),\n        logStructure: null,\n        keyLabel: '',\n        newLabel: '',\n        dateRange: null,\n        target: '0',\n    };\n    return items.concat(item);\n}\n\nfunction renderRow(props) {\n    const { item } = props;\n    const children = props.children || [];\n    return (\n        <div key={props.label}>\n            <InputGroup className=\"my-1\">\n                {children.shift()}\n                <TypeaheadSelector\n                    id={`long-term-goals-settings-row-${item.__id__}`}\n                    disabled={props.disabled}\n                    placeholder=\"Structure\"\n                    options={TypeaheadOptions.getFromTypes(['log-structure'])}\n                    value={item.logStructure}\n                    onChange={(logStructure) => props.onChange({ ...item, logStructure })}\n                />\n                <TextInput\n                    placeholder=\"Key Label\"\n                    value={item.keyLabel}\n                    disabled={props.disabled}\n                    onChange={(keyLabel) => props.onChange({ ...item, keyLabel })}\n                />\n                <TextInput\n                    placeholder=\"New Label\"\n                    value={item.newLabel}\n                    disabled={props.disabled}\n                    onChange={(newLabel) => props.onChange({ ...item, newLabel })}\n                />\n                {children.pop()}\n            </InputGroup>\n            <InputGroup className=\"my-1\">\n                <DateRangePicker\n                    dateRange={item.dateRange}\n                    onChange={(dateRange) => props.onChange({ ...item, dateRange })}\n                />\n                <TextInput\n                    placeholder=\"Target\"\n                    value={item.target}\n                    disabled={props.disabled}\n                    onChange={(target) => props.onChange({ ...item, target })}\n                />\n            </InputGroup>\n        </div>\n    );\n}\n\nfunction LongTermGoalsSettings(props) {\n    const items = props.value || [];\n    return (\n        <div className=\"my-3\">\n            <LeftRight>\n                <div>Long Term Goals</div>\n                <a href=\"#\" onClick={() => props.onChange(addNewItem(items))}>\n                    Add Entry\n                </a>\n            </LeftRight>\n            <SortableList\n                items={items}\n                disabled={props.disabled}\n                onChange={(newItems) => props.onChange(newItems)}\n                type={renderRow}\n                valueKey=\"item\"\n            />\n        </div>\n    );\n}\n\nLongTermGoalsSettings.propTypes = {\n    disabled: PropTypes.bool.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.any,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default LongTermGoalsSettings;\n"
  },
  {
    "path": "src/plugins/kaustubh/long_term_goals/client.js",
    "content": "import React from 'react';\n\nimport { PluginClient, PluginDisplayLocation } from '../../../client/Common';\nimport LongTermGoalGraph from './LongTermGoalGraph';\nimport LongTermGoalsSettings from './LongTermGoalsSettings';\n\nexport default class extends PluginClient {\n    static getSettingsKey() {\n        return 'long_term_goals';\n    }\n\n    static getSettingsComponent(props) {\n        return <LongTermGoalsSettings {...props} />;\n    }\n\n    static getDisplayLocation() {\n        return PluginDisplayLocation.TAB_SECTION;\n    }\n\n    static getTabData() {\n        return {\n            value: 'long_term_goals',\n            label: 'Long Term Goals',\n        };\n    }\n\n    static getDisplayComponent(props) {\n        return (props.settings || []).map((goal) => (\n            <LongTermGoalGraph\n                key={goal.newLabel}\n                goal={goal}\n            />\n        ));\n    }\n}\n"
  },
  {
    "path": "src/plugins/kaustubh/more_event_lists/MoreEventListsSettings.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport {\n    LeftRight, SortableList, TextInput,\n} from '../../../client/Common';\nimport { getNextID } from '../../../common/data_types';\n\n/*\n\nFIlters I want?\nTomorrow.\nNext 7 days.\nOverdue.\nMajor events in the future.\n\nDate: lt/gt\nDateRange: startDate, EndDate\n    DatePicker / DateRangePicker are not useful.\n    Pure Text for Date? lt(today)\nIsComplete?\nTopics / Structures\n\n*/\n\nfunction renderRow(props) {\n    const { item } = props;\n    const children = props.children || [];\n    return (\n        <div key={props.label}>\n            <InputGroup className=\"my-1\">\n                {children.shift()}\n                <TextInput\n                    placeholder=\"Label\"\n                    value={item.label}\n                    disabled={props.disabled}\n                    onChange={(label) => props.onChange({ ...item, label })}\n                />\n                {children.pop()}\n            </InputGroup>\n        </div>\n    );\n}\n\nfunction MoreEventListsSettings(props) {\n    const items = props.value || [];\n    return (\n        <div className=\"my-3\">\n            <LeftRight>\n                <div>More Event Lists</div>\n                <a\n                    href=\"#\"\n                    onClick={() => props.onChange(items.concat({\n                        __id__: getNextID(items),\n                        label: '',\n                        timezone: '',\n                    }))}\n                >\n                    Add Entry\n                </a>\n            </LeftRight>\n            <SortableList\n                items={items}\n                disabled={props.disabled}\n                onChange={(newItems) => props.onChange(newItems)}\n                type={renderRow}\n                valueKey=\"item\"\n            />\n        </div>\n    );\n}\n\nMoreEventListsSettings.propTypes = {\n    disabled: PropTypes.bool.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.any,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default MoreEventListsSettings;\n"
  },
  {
    "path": "src/plugins/kaustubh/more_event_lists/client.js",
    "content": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport MoreEventListsSettings from './MoreEventListsSettings';\n\nexport default class extends PluginClient {\n    static getSettingsKey() {\n        return 'more_event_lists';\n    }\n\n    static getSettingsComponent(props) {\n        return <MoreEventListsSettings {...props} />;\n    }\n\n    static getDisplayLocation() {\n        return null;\n    }\n\n    static getDisplayComponent(props) {\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/plugins/kaustubh/time_sections/TimeSection.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { LeftRight, SidebarSection } from '../../../client/Common';\n\nconst { formatToTimeZone } = require('date-fns-timezone');\n\nclass TimeSection extends React.Component {\n    constructor(props) {\n        super(props);\n        const offset = new Date().valueOf() % 1000;\n        this.timeout = window.setTimeout(() => {\n            this.interval = window.setInterval(() => this.forceUpdate(), 1000);\n        }, 1000 - offset);\n    }\n\n    componentWillUnmount() {\n        window.clearTimeout(this.timeout);\n        window.clearInterval(this.interval);\n    }\n\n    render() {\n        return (\n            <SidebarSection>\n                <LeftRight>\n                    {this.props.label}\n                    {formatToTimeZone(\n                        new Date(),\n                        'HH:mm:ss',\n                        { timeZone: this.props.timezone },\n                    )}\n                </LeftRight>\n            </SidebarSection>\n        );\n    }\n}\n\nTimeSection.propTypes = {\n    label: PropTypes.string.isRequired,\n    timezone: PropTypes.string.isRequired,\n};\n\nexport default TimeSection;\n"
  },
  {
    "path": "src/plugins/kaustubh/time_sections/TimeSectionSettings.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\nimport { listTimeZones } from 'timezone-support';\n\nimport {\n    LeftRight, Selector, SortableList, TextInput,\n} from '../../../client/Common';\nimport { getNextID } from '../../../common/data_types';\n\nconst TIMEZONE_OPTIONS = [{ label: '(timezone)', value: '' }].concat(Selector.getStringListOptions(listTimeZones().sort()));\n\nfunction renderRow(props) {\n    const { item } = props;\n    const children = props.children || [];\n    return (\n        <div key={props.label}>\n            <InputGroup className=\"my-1\">\n                {children.shift()}\n                <TextInput\n                    placeholder=\"Label\"\n                    value={item.label}\n                    disabled={props.disabled}\n                    onChange={(label) => props.onChange({ ...item, label })}\n                />\n                <Selector\n                    disabled={props.disabled}\n                    options={TIMEZONE_OPTIONS}\n                    value={item.timezone}\n                    onChange={(timezone) => props.onChange({ ...item, timezone })}\n                />\n                {children.pop()}\n            </InputGroup>\n        </div>\n    );\n}\n\nfunction TimeSectionSettings(props) {\n    const items = props.value;\n    return (\n        <div className=\"my-3\">\n            <LeftRight>\n                <div>Display Timezones</div>\n                <a\n                    href=\"#\"\n                    onClick={() => props.onChange(items.concat({\n                        __id__: getNextID(items),\n                        label: '',\n                        timezone: '',\n                    }))}\n                >\n                    Add Entry\n                </a>\n            </LeftRight>\n            <SortableList\n                items={items}\n                disabled={props.disabled}\n                onChange={(newItems) => props.onChange(newItems)}\n                type={renderRow}\n                valueKey=\"item\"\n            />\n        </div>\n    );\n}\n\nTimeSectionSettings.propTypes = {\n    disabled: PropTypes.bool.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.any,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default TimeSectionSettings;\n"
  },
  {
    "path": "src/plugins/kaustubh/time_sections/client.js",
    "content": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport TimeSection from './TimeSection';\nimport TimeSectionSettings from './TimeSectionSettings';\n\nexport default class extends PluginClient {\n    static getSettingsKey() {\n        return 'timezones';\n    }\n\n    static getSettingsComponent(props) {\n        return <TimeSectionSettings {...props} />;\n    }\n\n    static getDisplayLocation() {\n        return 'right_sidebar_main_top';\n    }\n\n    static getDisplayComponent(props) {\n        return (props.settings || [])\n            .map((item) => (\n                <TimeSection\n                    key={item.__id__}\n                    label={item.label}\n                    timezone={item.timezone}\n                />\n            ));\n    }\n}\n"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/TopicRemindersSection.js",
    "content": "import React from 'react';\n\nimport {\n    DataLoader, DateContext, Link, SidebarSection,\n} from '../../../client/Common';\nimport PropTypes from '../../../client/prop-types';\n\nclass TopicRemindersSection extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {};\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => ({\n                name: 'topic-reminders',\n                args: {\n                    todayLabel: this.props.todayLabel,\n                    logStructureId: this.props.logStructureId,\n                    thresholdDays: this.props.thresholdDays,\n                },\n            }),\n            onData: (result) => this.setState(result),\n        });\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    renderContents() {\n        const { logTopicAndDayCounts } = this.state;\n        if (!logTopicAndDayCounts) {\n            return 'Loading ...';\n        }\n        return logTopicAndDayCounts.map(({ logTopic, dayCount }) => (\n            <SidebarSection.Item key={logTopic.__id__}>\n                <Link logTopic={logTopic}>{logTopic.name}</Link>\n                {dayCount ? <span className=\"ml-1\">{`(${dayCount} days)`}</span> : null}\n            </SidebarSection.Item>\n        ));\n    }\n\n    render() {\n        const { logStructure } = this.state;\n        const suffix = logStructure ? `: ${logStructure.name}` : '';\n        return (\n            <SidebarSection title={`Reminders${suffix}`}>\n                {this.renderContents()}\n            </SidebarSection>\n        );\n    }\n}\n\nTopicRemindersSection.propTypes = {\n    todayLabel: PropTypes.string.isRequired,\n    logStructureId: PropTypes.number.isRequired,\n    thresholdDays: PropTypes.number.isRequired,\n};\n\nexport default DateContext.Wrapper(TopicRemindersSection);\n"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/TopicRemindersSectionSettings.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport {\n    HelpIcon, LeftRight, SortableList, TextInput,\n    TooltipElement, TypeaheadOptions, TypeaheadSelector,\n} from '../../../client/Common';\nimport { getNextID } from '../../../common/data_types';\n\nfunction renderRow(props) {\n    const { item } = props;\n    const children = props.children || [];\n    return (\n        <div key={props.label}>\n            <InputGroup className=\"my-1\">\n                {children.shift()}\n                <TypeaheadSelector\n                    id={`topic-reminders-settings-row-${item.__id__}`}\n                    disabled={props.disabled}\n                    placeholder=\"Structure\"\n                    options={TypeaheadOptions.getFromTypes(['log-structure'])}\n                    value={item.logStructure}\n                    onChange={(logStructure) => props.onChange({ ...item, logStructure })}\n                />\n                <TextInput\n                    placeholder=\"Threshold Days\"\n                    value={item.thresholdDays}\n                    disabled={props.disabled}\n                    onChange={(thresholdDays) => props.onChange({ ...item, thresholdDays })}\n                />\n                {children.pop()}\n            </InputGroup>\n        </div>\n    );\n}\n\nfunction TopicRemindersSettings(props) {\n    const helpText = (\n        'Add sections on the right sidebar for each structure selected here (eg - Conversation). '\n        + 'For each structure, look at the details to get a list of topics (eg - people), and then '\n        + 'check to see if there are any events with that structure and topic in the last X days. '\n        + 'If not, show a reminder about that topic (eg - speak to someone every X days).'\n    );\n    const items = props.value;\n    return (\n        <div className=\"my-3\">\n            <LeftRight>\n                <div>\n                    Reminder Sections\n                    <TooltipElement>\n                        <HelpIcon isShown />\n                        <span>{helpText}</span>\n                    </TooltipElement>\n                </div>\n                <a\n                    href=\"#\"\n                    onClick={() => props.onChange(items.concat({\n                        __id__: getNextID(items),\n                        logStructure: null,\n                        thresholdDays: '',\n                    }))}\n                >\n                    Add Entry\n                </a>\n            </LeftRight>\n            <SortableList\n                items={items}\n                disabled={props.disabled}\n                onChange={(newItems) => props.onChange(newItems)}\n                type={renderRow}\n                valueKey=\"item\"\n            />\n        </div>\n    );\n}\n\nTopicRemindersSettings.propTypes = {\n    disabled: PropTypes.bool.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.any,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default TopicRemindersSettings;\n"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/actions.js",
    "content": "/* eslint-disable func-names */\n\nimport { differenceInCalendarDays } from 'date-fns';\n\nimport DateUtils from '../../../common/DateUtils';\nimport RichTextUtils from '../../../common/RichTextUtils';\n\nconst ActionsRegistry = {};\n\nActionsRegistry['topic-reminders'] = async function ({\n    todayLabel,\n    logStructureId,\n    thresholdDays,\n}) {\n    const logStructure = await this.invoke.call(\n        this,\n        'log-structure-load',\n        { __id__: logStructureId },\n    );\n    const todayDate = DateUtils.getDate(todayLabel);\n    const logTopics = Object.values(\n        RichTextUtils.extractMentions(logStructure.details, 'log-topic'),\n    );\n    const logTopicAndDayCounts = await Promise.all(logTopics.map(async (logTopic) => {\n        // TODO: Fetch only the latest item from the database.\n        const logEvents = await this.invoke.call(\n            this,\n            'log-event-list',\n            {\n                where: { logStructure, logTopics: [logTopic], isComplete: true },\n                limit: 1,\n            },\n        );\n        const logEvent = logEvents.pop();\n        let dayCount = null;\n        let needsReminder = true;\n        if (logEvent) {\n            const lastDate = DateUtils.getDate(logEvent.date);\n            dayCount = differenceInCalendarDays(todayDate, lastDate);\n            needsReminder = dayCount > thresholdDays;\n        }\n        return needsReminder ? { logTopic, dayCount } : null;\n    }));\n    return { logStructure, logTopicAndDayCounts: logTopicAndDayCounts.filter((item) => item) };\n};\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/plugins/kaustubh/topic_reminder_sections/client.js",
    "content": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport TopicRemindersSection from './TopicRemindersSection';\nimport TopicRemindersSectionSettings from './TopicRemindersSectionSettings';\n\nexport default class extends PluginClient {\n    static getSettingsKey() {\n        return 'reminder_sections';\n    }\n\n    static getSettingsComponent(props) {\n        return <TopicRemindersSectionSettings {...props} />;\n    }\n\n    static getDisplayLocation() {\n        return 'right_sidebar_widgets_bottom';\n    }\n\n    static getDisplayComponent(props) {\n        return (props.settings || [])\n            .map((item) => (\n                <TopicRemindersSection\n                    key={item.__id__}\n                    logStructureId={item.logStructure.__id__}\n                    thresholdDays={parseInt(item.thresholdDays, 10)}\n                />\n            ));\n    }\n}\n"
  },
  {
    "path": "src/plugins/kaustubh/topic_sections/TopicSection.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\n\nimport { DataLoader, SidebarSection } from '../../../client/Common';\nimport RichTextUtils from '../../../common/RichTextUtils';\n\nclass TopicSection extends React.Component {\n    constructor(props) {\n        super(props);\n        this.state = {};\n    }\n\n    componentDidMount() {\n        this.dataLoader = new DataLoader({\n            getInput: () => ({\n                name: 'log-topic-load',\n                args: { __id__: this.props.logTopicId },\n            }),\n            onData: (logTopic) => this.setState({ logTopic }),\n        });\n    }\n\n    componentWillUnmount() {\n        this.dataLoader.stop();\n    }\n\n    renderContent() {\n        const { logTopic } = this.state;\n        if (!logTopic) {\n            return 'Loading ...';\n        }\n        // TODO: Update the style of bullet items in the TextEditor, use that instead.\n        const details = RichTextUtils.deserialize(\n            RichTextUtils.serialize(\n                logTopic.details,\n                RichTextUtils.StorageType.DRAFTJS,\n            ),\n            RichTextUtils.StorageType.MARKDOWN,\n        );\n        const lines = details.split('\\n')\n            .filter((line) => line.startsWith('- '))\n            .map((line) => line.substr(2));\n        return lines.map((item) => (\n            <SidebarSection.Item key={item}>\n                {item}\n            </SidebarSection.Item>\n        ));\n    }\n\n    render() {\n        const { logTopic } = this.state;\n        return (\n            <SidebarSection title={logTopic ? logTopic.name : '???'}>\n                {this.renderContent()}\n            </SidebarSection>\n        );\n    }\n}\n\nTopicSection.propTypes = {\n    logTopicId: PropTypes.number.isRequired,\n};\n\nexport default TopicSection;\n"
  },
  {
    "path": "src/plugins/kaustubh/topic_sections/TopicSectionSettings.js",
    "content": "import PropTypes from 'prop-types';\nimport React from 'react';\nimport InputGroup from 'react-bootstrap/InputGroup';\n\nimport {\n    HelpIcon, LeftRight, SortableList, TooltipElement, TypeaheadOptions, TypeaheadSelector,\n} from '../../../client/Common';\nimport { getNextID } from '../../../common/data_types';\n\nfunction renderRow(props) {\n    const { item } = props;\n    const children = props.children || [];\n    return (\n        <div key={props.label}>\n            <InputGroup className=\"my-1\">\n                {children.shift()}\n                <TypeaheadSelector\n                    id={`settings-topic-row-${item.__id__}`}\n                    disabled={props.disabled}\n                    options={TypeaheadOptions.getFromTypes(['log-topic'])}\n                    value={item.logTopic}\n                    onChange={(logTopic) => props.onChange({ ...item, logTopic })}\n                />\n                {children.pop()}\n            </InputGroup>\n        </div>\n    );\n}\n\nfunction TopicSectionSettings(props) {\n    const helpText = (\n        'Add sections on the right sidebar for each topic selected here, '\n        + 'which display the bullet-points in the details of those topics.'\n    );\n    const items = props.value;\n    return (\n        <div className=\"my-3\">\n            <LeftRight>\n                <div>\n                    Topic Sections\n                    <TooltipElement>\n                        <HelpIcon isShown />\n                        <span>{helpText}</span>\n                    </TooltipElement>\n                </div>\n                <a\n                    href=\"#\"\n                    onClick={() => props.onChange(items.concat({\n                        __id__: getNextID(items),\n                        logTopic: null,\n                    }))}\n                >\n                    Add Entry\n                </a>\n            </LeftRight>\n            <SortableList\n                items={items}\n                disabled={props.disabled}\n                onChange={(newItems) => props.onChange(newItems)}\n                type={renderRow}\n                valueKey=\"item\"\n            />\n        </div>\n    );\n}\n\nTopicSectionSettings.propTypes = {\n    disabled: PropTypes.bool.isRequired,\n    // eslint-disable-next-line react/forbid-prop-types\n    value: PropTypes.any,\n    onChange: PropTypes.func.isRequired,\n};\n\nexport default TopicSectionSettings;\n"
  },
  {
    "path": "src/plugins/kaustubh/topic_sections/client.js",
    "content": "import React from 'react';\n\nimport { PluginClient } from '../../../client/Common';\nimport TopicSection from './TopicSection';\nimport TopicSectionSettings from './TopicSectionSettings';\n\nexport default class extends PluginClient {\n    static getSettingsKey() {\n        return 'topic_sections';\n    }\n\n    static getSettingsComponent(props) {\n        return <TopicSectionSettings {...props} />;\n    }\n\n    static getDisplayLocation() {\n        return 'right_sidebar_widgets_top';\n    }\n\n    static getDisplayComponent(props) {\n        return (props.settings || [])\n            .map((item) => (\n                <TopicSection\n                    key={item.__id__}\n                    logTopicId={item.logTopic.__id__}\n                />\n            ));\n    }\n}\n"
  },
  {
    "path": "src/server/__tests__/Config.test.js",
    "content": "import fs from 'fs';\n\nconst CONFIG_FORMAT = {\n    '?lock': 'string',\n    database: {\n        dialect: 'string',\n        storage: 'string',\n        logging: 'boolean',\n    },\n    backup: {\n        location: 'string',\n    },\n    server: {\n        host: 'string',\n        port: 'number',\n    },\n    '?plugins': ['string'],\n};\n\nfunction check(pattern, value) {\n    if (Array.isArray(pattern)) {\n        expect(pattern.length).toEqual(1);\n        value.forEach((subvalue) => {\n            check(pattern[0], subvalue);\n        });\n    } else if (typeof pattern === 'object') {\n        expect(typeof value).toEqual('object');\n        expect(value).not.toBeNull();\n        Object.entries(pattern).forEach(([key, subpattern]) => {\n            if (key.startsWith('?')) {\n                key = key.slice(1);\n                if (Object.prototype.hasOwnProperty.call(value, key)) {\n                    check(subpattern, value[key]);\n                }\n            } else {\n                expect(Object.prototype.hasOwnProperty.call(value, key)).toEqual(true);\n                check(subpattern, value[key]);\n            }\n        });\n    } else if (typeof pattern === 'string') {\n        if (pattern.startsWith('?') && value !== null) {\n            check(value, pattern.slice(1));\n        } else {\n            expect(typeof value).toEqual(pattern);\n        }\n    }\n}\n\nfunction ensureValidConfig(configPath) {\n    if (fs.existsSync(configPath)) {\n        const contents = fs.readFileSync(configPath);\n        check(CONFIG_FORMAT, JSON.parse(contents));\n    }\n}\n\ntest('verify_config_structure', () => {\n    ensureValidConfig('config/example.glados.json');\n    ensureValidConfig('config/demo.glados.json');\n    ensureValidConfig('config.json');\n});\n"
  },
  {
    "path": "src/server/actions/__tests__/Backup.test.js",
    "content": "import tmp from 'tmp';\n\nimport { LogTopic } from '../../../common/data_types';\nimport TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\nfunction tmpDir() {\n    return new Promise((resolve, reject) => {\n        tmp.dir((error, path, _cleanup) => {\n            if (error) {\n                reject(error);\n            } else {\n                resolve(path);\n            }\n        });\n    }, { unsafeCleanup: true });\n}\n\ntest('test_backup', async () => {\n    const actions = await TestUtils.getActions();\n    const tempDirPath = await tmpDir();\n    actions.config = { backup: { location: tempDirPath } };\n    await actions.invoke('backup-save');\n\n    await TestUtils.loadData({\n        logTopics: [\n            { name: 'Hydrogen' },\n            { name: 'Helium' },\n            { name: 'Lithium' },\n            { name: 'Beryllium' },\n            { name: 'Boron' },\n        ],\n    });\n    expect((await actions.invoke('log-topic-list')).length).toEqual(5);\n    await actions.invoke('backup-save');\n    expect((await actions.invoke('log-topic-list')).length).toEqual(5);\n\n    await actions.invoke('log-topic-upsert', LogTopic.createVirtual({ name: 'Carbon' }));\n    await actions.invoke('log-topic-upsert', LogTopic.createVirtual({ name: 'Nitrogen' }));\n    await actions.invoke('log-topic-upsert', LogTopic.createVirtual({ name: 'Oxygen' }));\n    expect((await actions.invoke('log-topic-list')).length).toEqual(8);\n\n    await actions.invoke('backup-load');\n    expect((await actions.invoke('log-topic-list')).length).toEqual(5);\n});\n"
  },
  {
    "path": "src/server/actions/__tests__/Database.test.js",
    "content": "import TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\ntest('test_load_save_and_clear', async () => {\n    await TestUtils.loadData({\n        logTopics: [\n            { name: 'Mathematics' },\n            { name: 'Physics', parentTopicName: 'Mathematics' },\n            { name: 'Chemistry', parentTopicName: 'Physics' },\n            { name: 'English' },\n            { name: 'Computer Science', parentTopicName: 'Physics' },\n        ],\n    });\n\n    const actions = await TestUtils.getActions();\n    const data = await actions.invoke('database-load');\n    data.log_topics = data.log_topics.slice(0, -2);\n    await actions.invoke('database-save', data);\n    const logTopics = await actions.invoke('log-topic-list');\n    expect(logTopics.length).toEqual(3);\n\n    data.log_topics = data.log_topics.slice(1); // violates foreign key constraint\n    await expect(actions.invoke('database-save', data)).rejects.toThrow();\n});\n\ntest('test_data_format_version', async () => {\n    const actions = await TestUtils.getActions();\n    await actions.invoke('database-validate');\n    const data = await actions.invoke('database-load');\n    await actions.invoke('database-validate', { data });\n    data.settings[0].value += '+';\n    await expect(actions.invoke('database-validate', { data })).rejects.toThrow();\n});\n"
  },
  {
    "path": "src/server/actions/__tests__/LogEvent.test.js",
    "content": "import TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\ntest('test_structure_constraint', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'TestGroup' },\n        ],\n        logStructures: [\n            {\n                groupName: 'TestGroup',\n                name: 'Animals',\n                eventKeys: [\n                    { name: 'Size', type: 'string' },\n                    { name: 'Legs', type: 'integer' },\n                ],\n            },\n        ],\n        logEvents: [\n            {\n                date: '2020-06-28',\n                title: 'Cat',\n                structureName: 'Animals',\n                logValues: ['small', '4'],\n            },\n        ],\n    });\n\n    /*\n    const actions = TestUtils.getActions();\n    await expect(() => actions.invoke('log-structure-delete', 1)).rejects.toThrow();\n    await actions.invoke('log-event-delete', 1);\n    await actions.invoke('log-structure-delete', 1);\n    */\n});\n\ntest('test_event_update', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'TestGroup' },\n        ],\n        logStructures: [\n            {\n                groupName: 'TestGroup',\n                name: 'Animals',\n                eventKeys: [\n                    { name: 'Size', type: 'string' },\n                    { name: 'Legs', type: 'integer' },\n                ],\n            },\n        ],\n        logEvents: [\n            {\n                date: '2020-06-28',\n                title: 'Cat',\n                structureName: 'Animals',\n                logValues: ['small', '4'],\n            },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n\n    const logEvent = await actions.invoke('log-event-load', { __id__: 1 });\n    logEvent.title = 'Dog';\n    logEvent.logStructure.eventKeys[0].value = 'medium';\n    await actions.invoke('log-event-upsert', logEvent);\n});\n\ntest('test_log_event_value_typeahead', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'TestGroup' },\n        ],\n        logStructures: [\n            {\n                groupName: 'TestGroup',\n                name: 'Animals',\n                eventKeys: [\n                    { name: 'Size', type: 'string' },\n                    { name: 'Legs', type: 'integer' },\n                ],\n            },\n        ],\n        logEvents: [\n            {\n                date: '2020-06-28',\n                title: 'Cat',\n                structureName: 'Animals',\n                logValues: ['small', '4'],\n            },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n    let logValueSuggestions;\n\n    const logEvent = await actions.invoke('log-event-load', { __id__: 1 });\n    const input = { source: logEvent.logStructure, index: null, query: '' };\n\n    logValueSuggestions = await actions.invoke('value-typeahead', { ...input, index: 0 });\n    expect(logValueSuggestions).toEqual(['small']);\n\n    logValueSuggestions = await actions.invoke('value-typeahead', { ...input, index: 1 });\n    expect(logValueSuggestions).toEqual(['4']);\n});\n"
  },
  {
    "path": "src/server/actions/__tests__/LogStructure.test.js",
    "content": "import { LogKey } from '../../../common/data_types';\nimport RichTextUtils from '../../../common/RichTextUtils';\nimport TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\ntest('test_key_updates', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'Entertainment' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Entertainment',\n                name: 'Movie',\n                eventKeys: [\n                    { name: 'Title', type: 'string' },\n                    { name: 'Link', type: 'string' },\n                    { name: 'Rating', type: 'integer' },\n                ],\n                eventeventTitleTemplate: '$0: [$1]($2)',\n            },\n        ],\n        logEvents: [\n            {\n                date: '2020-08-23',\n                structureName: 'Movie',\n                logValues: ['The Martian', 'https://www.imdb.com/title/tt3659388/', '9'],\n            },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n\n    const oldLogEvent = await actions.invoke('log-event-load', { __id__: 1 });\n    const oldValues = oldLogEvent.logStructure.eventKeys.map((logKey) => logKey.value);\n\n    const logStructure = await actions.invoke('log-structure-load', { __id__: 1 });\n    const newLogKey = {\n        ...LogKey.createVirtual(),\n        name: 'Worthwhile?',\n        type: LogKey.Type.YES_OR_NO,\n        value: 'yes',\n    };\n    logStructure.eventKeys = [\n        logStructure.eventKeys[1],\n        logStructure.eventKeys[0],\n        newLogKey,\n    ];\n    await actions.invoke('log-structure-upsert', logStructure);\n\n    const newLogEvent = await actions.invoke('log-event-load', { __id__: 1 });\n    const newValues = newLogEvent.logStructure.eventKeys.map((logKey) => logKey.value);\n    expect(newValues[0]).toEqual(oldValues[1]);\n    expect(newValues[1]).toEqual(oldValues[0]);\n    expect(newValues[2]).toEqual(newLogKey.value);\n});\n\ntest('test_structure_deletion', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'Misc' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Misc',\n                name: 'Testingwa',\n                eventTitleTemplate: '$0',\n            },\n        ],\n    });\n    const actions = TestUtils.getActions();\n    await expect(() => actions.invoke('log-topic-delete', 1)).rejects.toThrow();\n    await actions.invoke('log-structure-delete', 1);\n    const logTopics = await actions.invoke('log-topic-list');\n    expect(logTopics.length).toEqual(0);\n});\n\ntest('test_structure_title_template_expression', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'Exercise' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Exercise',\n                name: 'Cycling',\n                eventKeys: [\n                    { name: 'Distance (miles)', type: 'integer' },\n                    { name: 'Time (minutes)', type: 'integer' },\n                ],\n                eventTitleTemplate: '$0: $1 miles / $2 minutes',\n            },\n        ],\n        logEvents: [\n            {\n                date: '2020-06-26',\n                structureName: 'Cycling',\n                logValues: ['15', '60'],\n            },\n            {\n                date: '2020-06-27',\n                structureName: 'Cycling',\n                logValues: ['15', '55'],\n            },\n            {\n                date: '2020-06-28',\n                structureName: 'Cycling',\n                logValues: ['15', '50'],\n            },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n    let logEvents = await actions.invoke('log-event-list');\n    expect(logEvents.map((logEvent) => RichTextUtils.extractPlainText(logEvent.title))).toEqual([\n        'Cycling: 15 miles / 60 minutes',\n        'Cycling: 15 miles / 55 minutes',\n        'Cycling: 15 miles / 50 minutes',\n    ]);\n\n    const { logStructure } = logEvents[0];\n    logStructure.eventTitleTemplate = RichTextUtils.convertPlainTextToDraftContent(\n        '$0: $1 miles / $2 minutes ({($1*60/$2).toFixed(2)} mph)',\n        { $: [logStructure, ...logStructure.eventKeys] },\n    );\n    await actions.invoke('log-structure-upsert', logStructure);\n\n    logEvents = await actions.invoke('log-event-list');\n    expect(logEvents.map((logEvent) => RichTextUtils.extractPlainText(logEvent.title))).toEqual([\n        'Cycling: 15 miles / 60 minutes (15.00 mph)',\n        'Cycling: 15 miles / 55 minutes (16.36 mph)',\n        'Cycling: 15 miles / 50 minutes (18.00 mph)',\n    ]);\n});\n\ntest('test_structure_title_template_link', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'Education' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Education',\n                name: 'Article',\n                eventKeys: [\n                    { name: 'Title', type: 'string' },\n                    { name: 'Link', type: 'string' },\n                ],\n                eventTitleTemplate: '$0: [$1]($2)',\n            },\n        ],\n        logEvents: [\n            {\n                date: '2020-08-23',\n                structureName: 'Article',\n                logValues: ['Facebook', 'https://facebook.com'],\n            },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n    const logEvents = await actions.invoke('log-event-list');\n    expect(RichTextUtils.extractPlainText(logEvents[0].title)).toEqual('Article: Facebook');\n});\n\ntest('test_structure_with_topic', async () => {\n    await TestUtils.loadData({\n        logTopics: [\n            { name: 'Books' },\n            { name: 'Harry Potter', parentTopicName: 'Books' },\n            { name: 'Foundation', parentTopicName: 'Books' },\n        ],\n        logStructureGroups: [\n            { name: 'Education' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Education',\n                name: 'Reading',\n                eventKeys: [\n                    { name: 'Book', type: 'log_topic', parentTopicName: 'Books' },\n                    { name: 'Progress', type: 'string' },\n                ],\n                eventTitleTemplate: '$0: $1 ($2)',\n            },\n        ],\n        logEvents: [\n            {\n                date: '2020-07-23',\n                structureName: 'Reading',\n                logValues: ['Harry Potter', '60'],\n            },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n    await expect(() => actions.invoke('log-topic-delete', 2)).rejects.toThrow();\n    const logEvent = await actions.invoke('log-event-load', { __id__: 1 });\n    logEvent.logStructure.eventKeys[0].value = await actions.invoke('log-topic-load', { __id__: 3 });\n    await actions.invoke('log-event-upsert', logEvent);\n    await actions.invoke('log-topic-delete', 2);\n});\n"
  },
  {
    "path": "src/server/actions/__tests__/LogTopic.test.js",
    "content": "import { asyncSequence } from '../../../common/AsyncUtils';\nimport { LogKey } from '../../../common/data_types';\nimport RichTextUtils from '../../../common/RichTextUtils';\nimport TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\ntest('test_log_topic_typeahead', async () => {\n    await TestUtils.loadData({\n        logTopics: [\n            { name: 'Anurag Dubey' },\n            { name: 'Kaustubh Karkare' },\n            { name: 'Vishnu Mohandas' },\n            { name: 'philosophy' },\n            { name: 'productivity' },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n    let logTopics;\n\n    logTopics = await actions.invoke('log-topic-typeahead', { query: '' });\n    expect(logTopics.length).toEqual(5);\n    logTopics = await actions.invoke('log-topic-typeahead', { query: 'k' });\n    expect(logTopics.length).toEqual(1);\n    logTopics = await actions.invoke('log-topic-typeahead', { query: 'p' });\n    expect(logTopics.length).toEqual(2);\n    logTopics = await actions.invoke('log-topic-typeahead', { query: 'i' });\n    expect(logTopics.length).toEqual(3); // appears in 3 different items\n    logTopics = await actions.invoke('log-topic-typeahead', { query: 'x' });\n    expect(logTopics.length).toEqual(0);\n});\n\ntest('test_update_propagation', async () => {\n    await TestUtils.loadData({\n        logTopics: [\n            { name: 'Hacky' },\n            { name: 'Todo', details: 'Speak to a #1' },\n        ],\n        logEvents: [\n            { date: '{today}', title: 'Spoke to a #1' },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n    let logEvent = await actions.invoke('log-event-load', { __id__: 1 });\n    expect(RichTextUtils.extractPlainText(logEvent.title)).toEqual('Spoke to a Hacky');\n    let logTopic = await actions.invoke('log-topic-load', { __id__: 2 });\n    expect(RichTextUtils.extractPlainText(logTopic.details)).toEqual('Speak to a Hacky');\n\n    const person = await actions.invoke('log-topic-load', { __id__: 1 });\n    person.name = 'Noob';\n    await actions.invoke('log-topic-upsert', person);\n\n    logEvent = await actions.invoke('log-event-load', logEvent);\n    expect(RichTextUtils.extractPlainText(logEvent.title)).toEqual('Spoke to a Noob');\n    logTopic = await actions.invoke('log-topic-load', logTopic);\n    expect(RichTextUtils.extractPlainText(logTopic.details)).toEqual('Speak to a Noob');\n\n    await expect(() => actions.invoke('log-topic-delete', person.__id__)).rejects.toThrow();\n    await actions.invoke('log-event-delete', logEvent.__id__);\n    await actions.invoke('log-topic-delete', logTopic.__id__);\n    await actions.invoke('log-topic-delete', person.__id__);\n});\n\ntest('test_child_keys', async () => {\n    await TestUtils.loadData({\n        logTopics: [\n            { name: 'Movies' },\n            { name: 'The Martian', parentTopicName: 'Movies' },\n            { name: 'Inside Out', parentTopicName: 'Movies' },\n            { name: 'Bhool Bhulaiyaa 2', parentTopicName: 'Movies' },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n    let parentLogTopic = await actions.invoke('log-topic-load', { __id__: 1 });\n    const newLogKey = {\n        ...LogKey.createVirtual(),\n        name: 'Worthwhile?',\n        type: LogKey.Type.YES_OR_NO,\n        // value: 'yes',\n    };\n    parentLogTopic.childKeys = [newLogKey];\n    await expect(() => actions.invoke('log-topic-upsert', parentLogTopic)).rejects.toThrow();\n    parentLogTopic.childKeys[0].value = 'yes';\n    parentLogTopic = await actions.invoke('log-topic-upsert', parentLogTopic);\n\n    let childLogTopic = await actions.invoke('log-topic-load', { __id__: 4 });\n    childLogTopic.parentLogTopic.childKeys[0].value = 'no';\n    childLogTopic = await actions.invoke('log-topic-upsert', childLogTopic);\n});\n\ntest('test_counts', async () => {\n    await TestUtils.loadData({\n        logTopics: [\n            { name: 'Parent1' },\n            { name: 'Parent2' },\n            { name: 'Child', parentTopicName: 'Parent1' },\n        ],\n    });\n\n    const actions = TestUtils.getActions();\n\n    const parentLogTopicIds = [1, 2];\n    const expectChildCounts = async (counts) => {\n        await asyncSequence(\n            parentLogTopicIds,\n            async (id, index) => {\n                const parentLogTopic = await actions.invoke('log-topic-load', { __id__: id });\n                expect(parentLogTopic.childCount).toEqual(counts[index]);\n            },\n        );\n    };\n    await expectChildCounts([1, 0]);\n\n    let childLogTopic = await actions.invoke('log-topic-load', { __id__: 3 });\n    expect(childLogTopic.parentLogTopic.__id__).toEqual(1);\n    childLogTopic.parentLogTopic.__id__ = 2;\n    childLogTopic = await actions.invoke('log-topic-upsert', childLogTopic);\n    expect(childLogTopic.parentLogTopic.__id__).toEqual(2);\n    await expectChildCounts([0, 1]);\n\n    await actions.invoke('log-topic-delete', childLogTopic.__id__);\n    await expectChildCounts([0, 0]);\n});\n"
  },
  {
    "path": "src/server/actions/__tests__/Reminders.test.js",
    "content": "import TestUtils from './TestUtils';\n\nbeforeEach(TestUtils.beforeEach);\nafterEach(TestUtils.afterEach);\n\nasync function checkIfReminderIsShown(todayLabel, shown) {\n    const actions = TestUtils.getActions();\n    const results = await actions.invoke('reminder-sidebar', { todayLabel });\n    expect(results.length).toEqual(shown ? 1 : 0);\n}\n\ntest('test_reminder_without_warning', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'Daily Routine' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Daily Routine',\n                name: 'Exercise',\n                isPeriodic: true,\n                frequency: 'everyday',\n                warningDays: 0,\n                suppressUntilDate: '2020-08-07',\n            },\n        ],\n    });\n    await checkIfReminderIsShown('2020-08-08', true);\n    await TestUtils.loadData({\n        logEvents: [\n            {\n                date: '2020-08-08',\n                structureName: 'Exercise',\n            },\n        ],\n    });\n    await checkIfReminderIsShown('2020-08-08', false);\n});\n\ntest('test_reminder_with_warning', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'Birthdays' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Birthdays',\n                name: 'My Birthday',\n                eventTitleTemplate: '$0',\n                isPeriodic: true,\n                frequency: 'yearly',\n                frequencyArgs: '08-12',\n                warningDays: 7,\n                suppressUntilDate: '2020-01-01',\n            },\n        ],\n        logEvents: [\n            {\n                date: '2019-08-12',\n                structureName: 'My Birthday',\n            },\n        ],\n    });\n    await checkIfReminderIsShown('2020-08-01', false);\n    await checkIfReminderIsShown('2020-08-05', true);\n    await checkIfReminderIsShown('2020-08-12', true);\n    await checkIfReminderIsShown('2020-08-15', true);\n    await TestUtils.loadData({\n        logEvents: [\n            {\n                date: '2020-08-15',\n                structureName: 'My Birthday',\n            },\n        ],\n    });\n    await checkIfReminderIsShown('2020-08-15', false);\n});\n\nasync function checkReminderScore(todayLabel, value, deadline) {\n    const actions = TestUtils.getActions();\n    const logStructure = await actions.invoke('log-structure-load', { __id__: 1 });\n    const score = await actions.invoke('reminder-score', { logStructure, todayLabel });\n    expect(score.value).toEqual(value);\n    expect(score.deadline).toEqual(deadline);\n}\n\ntest('test_reminder_score', async () => {\n    await TestUtils.loadData({\n        logStructureGroups: [\n            { name: 'Weekly' },\n        ],\n        logStructures: [\n            {\n                groupName: 'Weekly',\n                name: 'Weekly Report',\n                isPeriodic: true,\n                frequency: 'friday',\n                warningDays: 2, // warning starts on wednesday\n                suppressUntilDate: '2020-08-20',\n            },\n        ],\n    });\n    const addEvent = (date) => TestUtils.loadData({\n        logEvents: [{ date, structureName: 'Weekly Report' }],\n    });\n    await checkReminderScore('2020-08-15', 0, null);\n    await checkReminderScore('2020-08-20', 0, null);\n    // week 1: event date = reminder date\n    await addEvent('2020-08-21'); // friday\n    await checkReminderScore('2020-08-21', 1, null); // friday\n    // week 2: event date < reminder date\n    await checkReminderScore('2020-08-25', 1, null); // tuesday\n    await checkReminderScore('2020-08-26', 1, '2020-09-01'); // wednesday\n    await addEvent('2020-08-27'); // thursday\n    await checkReminderScore('2020-08-28', 2, null); // friday\n    // week 3: event date > reminder date\n    await checkReminderScore('2020-09-04', 2, '2020-09-08'); // friday\n    await addEvent('2020-09-05'); // saturday\n    // week 4\n    await checkReminderScore('2020-09-06', 3, null); // sunday\n    await checkReminderScore('2020-09-11', 3, '2020-09-15'); // friday\n    // week 5\n    await checkReminderScore('2020-09-18', -1, '2020-09-22'); // friday\n    // week 6\n    await checkReminderScore('2020-09-25', -2, '2020-09-29'); // friday\n    await addEvent('2020-09-25'); // friday\n    await checkReminderScore('2020-09-26', 1, null); // saturday\n});\n"
  },
  {
    "path": "src/server/actions/__tests__/TestUtils.js",
    "content": "import { asyncSequence } from '../../../common/AsyncUtils';\nimport { getVirtualID, LogKey } from '../../../common/data_types';\nimport DateUtils from '../../../common/DateUtils';\nimport RichTextUtils from '../../../common/RichTextUtils';\nimport Actions from '../../actions';\nimport Database from '../../database';\n\nlet actions = null;\n\nfunction getBool(item, key, defaultValue) {\n    return typeof item[key] === 'undefined' ? defaultValue : item[key];\n}\n\nexport default class TestUtils {\n    static async beforeEach() {\n        const config = {\n            dialect: 'sqlite',\n            storage: ':memory:',\n            logging: false,\n        };\n        const database = new Database(config);\n        actions = new Actions(null, database);\n        await actions.invoke('database-reset');\n    }\n\n    static getActions() {\n        return actions;\n    }\n\n    static async afterEach() {\n        if (actions) await actions.database.close();\n    }\n\n    static async loadData(data) {\n        const { todayDate } = DateUtils.getContext();\n        const logTopicMap = {};\n        const logTopics = [null];\n        const existingLogTopics = await actions.invoke('log-topic-list');\n        existingLogTopics.forEach((outputLogTopic) => {\n            logTopicMap[outputLogTopic.name] = outputLogTopic;\n            logTopics.push(outputLogTopic);\n        });\n        await asyncSequence(data.logTopics, async (inputLogTopic) => {\n            inputLogTopic.__id__ = getVirtualID();\n            if (inputLogTopic.parentTopicName) {\n                inputLogTopic.parentLogTopic = logTopicMap[inputLogTopic.parentTopicName];\n                delete inputLogTopic.parentTopicName;\n            }\n            inputLogTopic.details = RichTextUtils.convertPlainTextToDraftContent(\n                inputLogTopic.details || '',\n                { '#': logTopics },\n            );\n            inputLogTopic.isFavorite = false;\n            inputLogTopic.isDeprecated = false;\n            inputLogTopic.hasStructure = false;\n            const outputLogTopic = await actions.invoke('log-topic-upsert', inputLogTopic);\n            logTopicMap[outputLogTopic.name] = outputLogTopic;\n            logTopics.push(outputLogTopic);\n        });\n\n        const logStructureGroupMap = {};\n        const existingLogStructureGroups = await actions.invoke('log-structure-group-list');\n        existingLogStructureGroups.forEach((outputLogStructureGroup) => {\n            logStructureGroupMap[outputLogStructureGroup.name] = outputLogStructureGroup;\n        });\n        await asyncSequence(data.logStructureGroups, async (inputLogStructureGroup) => {\n            inputLogStructureGroup.__id__ = getVirtualID();\n            const outputLogStructureGroup = await actions.invoke(\n                'log-structure-group-upsert',\n                inputLogStructureGroup,\n            );\n            logStructureGroupMap[outputLogStructureGroup.name] = outputLogStructureGroup;\n        });\n\n        const logStructureMap = {};\n        const existingLogStructures = await actions.invoke('log-structure-list');\n        existingLogStructures.forEach((outputLogStructure) => {\n            logStructureMap[outputLogStructure.name] = outputLogStructure;\n        });\n        await asyncSequence(data.logStructures, async (inputLogStructure) => {\n            inputLogStructure.__type__ = 'log-structure';\n            inputLogStructure.__id__ = getVirtualID();\n            inputLogStructure.logStructureGroup = logStructureGroupMap[inputLogStructure.groupName];\n            delete inputLogStructure.groupName;\n            inputLogStructure.details = '';\n            inputLogStructure.eventAllowDetails = true;\n            if (inputLogStructure.eventKeys) {\n                inputLogStructure.eventKeys.forEach((logKey, index) => {\n                    logKey.__type__ = 'log-structure-key';\n                    logKey.__id__ = index + 1;\n                    if (logKey.parentTopicName) {\n                        logKey.parentLogTopic = logTopicMap[logKey.parentTopicName];\n                        delete logKey.parentTopicName;\n                    }\n                });\n            } else {\n                inputLogStructure.eventKeys = [];\n            }\n            inputLogStructure.eventTitleTemplate = RichTextUtils.convertPlainTextToDraftContent(\n                inputLogStructure.eventTitleTemplate || '$0',\n                { $: [inputLogStructure, ...inputLogStructure.eventKeys] },\n            );\n            inputLogStructure.eventNeedsEdit = inputLogStructure.eventNeedsEdit || false;\n            inputLogStructure.isFavorite = false;\n            inputLogStructure.isDeprecated = false;\n\n            inputLogStructure.isPeriodic = inputLogStructure.isPeriodic || false;\n            inputLogStructure.reminderText = inputLogStructure.reminderText || null;\n            inputLogStructure.frequency = inputLogStructure.frequency || null;\n            inputLogStructure.frequencyArgs = inputLogStructure.frequencyArgs || null;\n            inputLogStructure.warningDays = inputLogStructure.isPeriodic\n                ? (inputLogStructure.warningDays || 0)\n                : null;\n            inputLogStructure.suppressUntilDate = inputLogStructure.suppressUntilDate || null;\n            DateUtils.maybeSubstitute(todayDate, inputLogStructure, 'suppressUntilDate');\n\n            inputLogStructure.logLevel = 0;\n            const outputLogStructure = await actions.invoke('log-structure-upsert', inputLogStructure);\n            logStructureMap[outputLogStructure.name] = outputLogStructure;\n        });\n\n        await asyncSequence(data.logEvents, async (inputLogEvent) => {\n            inputLogEvent.__id__ = getVirtualID();\n            DateUtils.maybeSubstitute(todayDate, inputLogEvent, 'date');\n            inputLogEvent.title = RichTextUtils.convertPlainTextToDraftContent(\n                inputLogEvent.title || '',\n                { '#': logTopics },\n            );\n            inputLogEvent.details = RichTextUtils.convertPlainTextToDraftContent(\n                inputLogEvent.details || '',\n                { '#': logTopics },\n            );\n            inputLogEvent.logLevel = 0;\n            inputLogEvent.isFavorite = false;\n            inputLogEvent.isComplete = getBool(inputLogEvent, 'isComplete', true);\n            if (inputLogEvent.structureName) {\n                inputLogEvent.logStructure = logStructureMap[inputLogEvent.structureName];\n                if (inputLogEvent.logValues) {\n                    inputLogEvent.logValues.forEach((value, index) => {\n                        const logKey = inputLogEvent.logStructure.eventKeys[index];\n                        if (logKey.type === LogKey.Type.LOG_TOPIC) {\n                            logKey.value = logTopicMap[value];\n                        } else {\n                            logKey.value = value;\n                        }\n                    });\n                }\n            }\n            await actions.invoke('log-event-upsert', inputLogEvent);\n        });\n    }\n}\n"
  },
  {
    "path": "src/server/actions/backup.js",
    "content": "/* eslint-disable func-names */\n\nimport assert from 'assert';\nimport crypto from 'crypto';\nimport fs from 'fs';\nimport path from 'path';\n\nimport { callbackToPromise } from '../../common/AsyncUtils';\n\nfunction getDateAndTime() {\n    const date = new Date();\n    let dateLabel = date.getFullYear();\n    dateLabel += (`0${(date.getMonth() + 1)}`).substr(-2);\n    dateLabel += (`0${date.getDate()}`).substr(-2);\n    let timeLabel = (`0${date.getHours()}`).substr(-2);\n    timeLabel += (`0${date.getMinutes()}`).substr(-2);\n    timeLabel += (`0${date.getSeconds()}`).substr(-2);\n    return { date: dateLabel, time: timeLabel };\n}\n\nfunction parseDateAndTime(date, time) {\n    return `${date.substr(0, 4)\n    }-${date.substr(4, 2)\n    }-${date.substr(6, 2)\n    } ${time.substr(0, 2)\n    }:${time.substr(2, 2)\n    }:${time.substr(4, 2)}`;\n}\n\nfunction getFileName({ date, time, hash }) {\n    return `backup-${date}-${time}-${hash}.json`;\n}\n\nfunction parseFileName(filename) {\n    const matchResult = filename.match(/^backup-(\\d+)-(\\d+)-(\\w+)\\.json$/);\n    return {\n        hash: matchResult[3],\n        timetamp: parseDateAndTime(matchResult[1], matchResult[2]),\n    };\n}\n\n// Intermediate Operations.\n\nconst ActionsRegistry = {};\n\nActionsRegistry['backup-file-load'] = async function ({ filename }) {\n    const filedata = await callbackToPromise(\n        fs.readFile,\n        path.join(this.config.backup.location, filename),\n    );\n    return JSON.parse(filedata);\n};\n\nActionsRegistry['backup-file-save'] = async function ({ data }) {\n    const { date, time } = getDateAndTime();\n\n    const dataSerialized = JSON.stringify(data, null, '\\t');\n    const hash = crypto.createHash('md5').update(dataSerialized).digest('hex');\n\n    try {\n        const latestBackup = await this.invoke.call(this, 'backup-latest');\n        if (latestBackup && hash === latestBackup.hash) {\n            return { ...latestBackup, isUnchanged: true };\n        }\n    } catch (error) {\n        assert(error.message === 'no backups found');\n    }\n\n    const filename = getFileName({ date, time, hash });\n    await callbackToPromise(\n        fs.writeFile,\n        path.join(this.config.backup.location, filename),\n        dataSerialized,\n    );\n    this.broadcast('backup-latest');\n    return {\n        filename, date, time, hash,\n    };\n};\n\nActionsRegistry['backup-transform-data'] = async function (data) {\n    // return this.invoke.call(this, 'transformation-method', data);\n    return { data };\n};\n\n// Actual API\n\nActionsRegistry['backup-save'] = async function ({ verbose } = {}) {\n    const data = await this.invoke.call(this, 'database-load');\n    const result = await this.invoke.call(this, 'backup-file-save', { data });\n    if (verbose) {\n        // eslint-disable-next-line no-console\n        console.info(`Saved ${result.filename}${result.isUnchanged ? ' (unchanged)' : ''}`);\n    }\n    return result;\n};\n\nActionsRegistry['backup-latest'] = async function () {\n    let filenames = await callbackToPromise(fs.readdir, this.config.backup.location);\n    filenames = filenames.filter((filename) => filename.startsWith('backup-')).sort();\n    if (!filenames.length) {\n        return null;\n    }\n    assert(filenames.length, 'no backups found');\n    const filename = filenames[filenames.length - 1];\n    const components = parseFileName(filename);\n    return { filename, ...components };\n};\n\nActionsRegistry['backup-load'] = async function ({ verbose } = {}) {\n    const latestBackup = await this.invoke.call(this, 'backup-latest');\n    assert(latestBackup, 'at least one backup is required');\n    let data = await this.invoke.call(this, 'backup-file-load', { filename: latestBackup.filename });\n    const transformationResult = await this.invoke.call(this, 'backup-transform-data', data);\n    data = transformationResult.data;\n    await this.invoke.call(this, 'database-validate', { data });\n    await this.invoke.call(this, 'database-save', data);\n    if (verbose) {\n        // eslint-disable-next-line no-console\n        console.info(`Loaded ${latestBackup.filename}`);\n    }\n    if (transformationResult.validate) {\n        await transformationResult.validate();\n    }\n    return latestBackup;\n};\n\nActionsRegistry['backup-delete'] = async function ({ filename }) {\n    return callbackToPromise(fs.unlink, path.join(this.config.backup.location, filename));\n};\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/server/actions/data_types.js",
    "content": "/* eslint-disable func-names */\n\nimport { getDataTypeMapping } from '../../common/data_types';\n\nconst ActionsRegistry = {};\n\nObject.entries(getDataTypeMapping()).forEach((pair) => {\n    const [name, DataType] = pair;\n    ActionsRegistry[`${name}-list`] = async function (input) {\n        const context = { ...this, DataType };\n        input = input || {};\n        const where = input.where || {};\n        await DataType.updateWhere.call(context, where);\n        return DataType.list.call(context, where, input.limit);\n    };\n    ActionsRegistry[`${name}-typeahead`] = async function ({ query, where = {} }) {\n        const context = { ...this, DataType };\n        await DataType.updateWhere.call(context, where);\n        return DataType.typeahead.call(context, { query, where });\n    };\n    ActionsRegistry[`${name}-validate`] = async function (input) {\n        const context = { ...this, DataType };\n        return DataType.getValidationErrors.call(context, input);\n    };\n    ActionsRegistry[`${name}-load-partial`] = async function (input) {\n        const context = { ...this, DataType };\n        return DataType.loadPartial.call(context, input.__id__);\n    };\n    ActionsRegistry[`${name}-load`] = async function (input) {\n        const context = { ...this, DataType };\n        return DataType.load.call(context, input.__id__);\n    };\n    ActionsRegistry[`${name}-reorder`] = async function (input) {\n        const context = { ...this, DataType };\n        return DataType.reorder.call(context, input);\n    };\n    ActionsRegistry[`${name}-sort`] = async function (input) {\n        const context = { ...this, DataType };\n        await DataType.updateWhere.call(context, input.where);\n        return DataType.sort.call(context, input);\n    };\n    ActionsRegistry[`${name}-upsert`] = async function (input) {\n        const context = { ...this, DataType };\n        if (DataType.trigger) {\n            DataType.trigger.call(context, input);\n        }\n        const errors = await DataType.getValidationErrors.call(context, input);\n        if (errors.length) {\n            throw new Error(`${errors.join('\\n')}\\n${JSON.stringify(input, null, 4)}`);\n        }\n        const id = await DataType.save.call(context, input);\n        // This informs the client-side DataLoader.\n        this.broadcast(`${name}-load`, { __id__: id });\n        this.broadcast(`${name}-list`, { where: { __id__: id } });\n        return DataType.load.call(context, id);\n    };\n    ActionsRegistry[`${name}-delete`] = async function (id) {\n        const context = { ...this, DataType };\n        // This informs the client-side DataLoader.\n        this.broadcast(`${name}-load`, { __id__: id });\n        this.broadcast(`${name}-list`, { where: { __id__: id } });\n        return DataType.delete.call(context, id);\n    };\n});\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/server/actions/database.js",
    "content": "/* eslint-disable func-names */\n\nimport assert from 'assert';\nimport toposort from 'toposort';\n\nimport { asyncSequence } from '../../common/AsyncUtils';\nimport { getDataFormatVersion } from '../models';\n\nconst ActionsRegistry = {};\n\nActionsRegistry['database-load'] = async function () {\n    // Since the database format might not be in-sync with the code,\n    // use the QueryInterface to load the data, instead of models.\n    const { sequelize } = this.database;\n    const api = sequelize.getQueryInterface();\n    const tableNames = await api.showAllTables();\n    const data = {};\n    await asyncSequence(tableNames, async (tableName) => {\n        data[tableName] = await sequelize.query(\n            `SELECT * FROM ${tableName}`,\n            { type: sequelize.QueryTypes.SELECT },\n        );\n    });\n    return data;\n};\n\nActionsRegistry['database-save'] = async function (data) {\n    await this.database.reset();\n    await asyncSequence(this.database.getModelSequence(), async (model) => {\n        const items = data[model.name] || [];\n        if (model.name !== 'log_topics') {\n            await asyncSequence(items, async (item) => {\n                try {\n                    await model.create(item, { transaction: this.database.transaction });\n                } catch (error) {\n                    // eslint-disable-next-line no-constant-condition\n                    if (false) {\n                        // eslint-disable-next-line no-console\n                        console.error(model.name, item);\n                    }\n                    throw error;\n                }\n            });\n        } else {\n            await model.bulkCreate(items, { transaction: this.database.transaction });\n        }\n    });\n};\n\nconst DATA_FORMAT_VERSION_KEY = '__data_format_version__';\n\nActionsRegistry['database-reset'] = async function ({ verbose = false } = {}) {\n    await this.database.reset();\n    await this.database.createOrUpdateItem('Settings', null, {\n        key: DATA_FORMAT_VERSION_KEY,\n        value: getDataFormatVersion(),\n    });\n    if (verbose) {\n        // eslint-disable-next-line no-console\n        console.info('Reset database!');\n    }\n};\n\nActionsRegistry['database-validate'] = async function ({ data: backupData, verbose } = {}) {\n    const expectedValue = getDataFormatVersion();\n    let actualValue = null;\n    if (backupData) {\n        const item = backupData.settings.find((row) => row.key === DATA_FORMAT_VERSION_KEY);\n        actualValue = item.value;\n    } else {\n        const item = await this.database.findOne('Settings', { key: DATA_FORMAT_VERSION_KEY });\n        actualValue = item.value;\n    }\n    assert(\n        expectedValue === actualValue,\n        `Data format version mismatch! Expected = ${expectedValue}, Actual = ${actualValue}`,\n    );\n    if (verbose) {\n        // eslint-disable-next-line no-console\n        console.info('Data format version validated!');\n    }\n};\n\nActionsRegistry['database-clear'] = async function () {\n    // For some reason, calling \"database-reset\" causes SQLITE_READONLY error.\n    // So this method is specifically designed for the demo videos.\n    const models = this.database.getModelSequence().slice().reverse();\n    await asyncSequence(models, async (model) => {\n        if (model.name === 'log_topics') {\n            // Since topics can reference other topics, the order of deletion matters.\n            // Using topological sort to avoid violating foreign key constraints.\n            const logTopics = await model.findAll();\n            const logTopicMap = {};\n            const nodes = [];\n            const edges = [];\n            logTopics.forEach((logTopic) => {\n                logTopicMap[logTopic.id] = logTopic;\n                nodes.push(logTopic.id);\n                if (logTopic.parent_topic_id) {\n                    edges.push([logTopic.parent_topic_id, logTopic.id]);\n                }\n            });\n            const result = toposort.array(nodes, edges).reverse();\n            await asyncSequence(result, async (id) => {\n                await logTopicMap[id].destroy();\n            });\n        }\n        try {\n            await model.sync({ force: true });\n        } catch (error) {\n            throw new Error(`${model.name} // ${error.message}`);\n        }\n    });\n};\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/server/actions/reminders.js",
    "content": "/* eslint-disable func-names */\n\nimport assert from 'assert';\nimport { addDays, compareAsc, subDays } from 'date-fns';\n\nimport { asyncFilter } from '../../common/AsyncUtils';\nimport { LogStructure } from '../../common/data_types';\nimport DateUtils from '../../common/DateUtils';\n\nconst ActionsRegistry = {};\n\nActionsRegistry['latest-log-event'] = async function (input) {\n    return this.database.findOne(\n        'LogEvent',\n        {\n            structure_id: input.logStructure.__id__,\n            date: { [this.database.Op.ne]: null },\n        },\n        [['date', 'DESC']],\n    );\n};\n\nActionsRegistry['reminder-check'] = async function (input) {\n    // This action is invoked by \"reminder-sidebar\".\n    const { logStructure, todayLabel } = input;\n    assert(logStructure.isPeriodic);\n\n    // If the reminder is suppressed, return early.\n    const todayDate = DateUtils.getDate(todayLabel);\n    const suppressUntilDate = DateUtils.getDate(logStructure.suppressUntilDate);\n    if (compareAsc(todayDate, suppressUntilDate) <= 0) {\n        return false;\n    }\n\n    // If the warning start date is in the future, return early.\n    const option = LogStructure.Frequency[logStructure.frequency];\n    const lookaheadDate = addDays(todayDate, 1 + logStructure.warningDays);\n    const reminderDate = option.getPreviousMatch(lookaheadDate, logStructure.frequencyArgs);\n    const warningStartDate = subDays(reminderDate, logStructure.warningDays);\n    const isWarningActive = compareAsc(warningStartDate, todayDate) <= 0;\n    if (!isWarningActive) return false;\n\n    // If there was an event since the warning start date, return early.\n    const latestLogEvent = await this.invoke.call(this, 'latest-log-event', { logStructure });\n    if (latestLogEvent) {\n        const latestLogEventDate = DateUtils.getDate(latestLogEvent.date);\n        if (compareAsc(warningStartDate, latestLogEventDate) <= 0) {\n            return false;\n        }\n    }\n\n    return true;\n};\n\nActionsRegistry['reminder-score'] = async function (input) {\n    // This action is invoked by \"reminder-sidebar\".\n    const { logStructure, todayLabel } = input;\n    assert(logStructure.isPeriodic);\n\n    // While the reminder-check is O(1), this operation is O(n).\n    const logEvents = await this.database.findAll(\n        'LogEvent',\n        {\n            structure_id: logStructure.__id__,\n            date: { [this.database.Op.ne]: null },\n        },\n        [['date', 'DESC']],\n    );\n\n    // The \"window of opportunity\" is defined as the start of the warning for one reminder,\n    // to the start of the warning for the next reminder.\n    const option = LogStructure.Frequency[logStructure.frequency];\n    const todayDate = DateUtils.getDate(todayLabel);\n    const nextReminderDate = option.getNextMatch(\n        // Using addDays here, so that deadlineDate will be in the future.\n        addDays(todayDate, logStructure.warningDays),\n        logStructure.frequencyArgs,\n    );\n    const deadlineDate = subDays(nextReminderDate, 1 + logStructure.warningDays);\n\n    // Start from the current window, and then go as far back as needed to compute the score.\n    let currentDate = addDays(todayDate, 1 + logStructure.warningDays);\n    let value = 0;\n    let deadline = null;\n    const dateRanges = [];\n    let firstIteration = true;\n    while (logEvents.length) {\n        const reminderDate = option.getPreviousMatch(currentDate, logStructure.frequencyArgs);\n        const warningStartDate = subDays(reminderDate, logStructure.warningDays);\n        let foundLogEventInReminderWindow = false;\n        while (logEvents.length) { // loop in case of multiple events in one window.\n            const logEventDate = DateUtils.getDate(logEvents[0].date);\n            if (compareAsc(logEventDate, currentDate) < 0) {\n                foundLogEventInReminderWindow = compareAsc(warningStartDate, logEventDate) <= 0;\n                break;\n            } else {\n                logEvents.shift();\n            }\n        }\n        let dateRange = `${DateUtils.getLabel(warningStartDate)}`;\n        if (compareAsc(warningStartDate, subDays(currentDate, 1)) < 0) {\n            dateRange += ` to ${DateUtils.getLabel(currentDate)}`;\n        }\n        currentDate = warningStartDate;\n        if (firstIteration) { // special handling for currently open window.\n            firstIteration = false;\n            if (!foundLogEventInReminderWindow) {\n                deadline = DateUtils.getLabel(deadlineDate);\n                // eslint-disable-next-line no-continue\n                continue;\n            }\n        }\n        if (foundLogEventInReminderWindow) {\n            if (value >= 0) {\n                value += 1;\n                dateRanges.push(dateRange);\n            } else {\n                break;\n            }\n        } else {\n            // eslint-disable-next-line no-lonely-if\n            if (value <= 0) {\n                value -= 1;\n                dateRanges.push(dateRange);\n            } else {\n                break;\n            }\n        }\n    }\n\n    return { value, deadline, dateRanges };\n};\n\nActionsRegistry['reminder-sidebar'] = async function (input) {\n    const { todayLabel } = input;\n    const logStructureGroups = await this.invoke.call(this, 'log-structure-group-list', {\n        ordering: true,\n        where: input.where,\n    });\n    const periodicLogStructures = await this.invoke.call(this, 'log-structure-list', {\n        where: { isPeriodic: true },\n        ordering: true,\n    });\n    const reminderGroups = await Promise.all(\n        logStructureGroups.map(async (logStructureGroup) => {\n            const logStructures = await asyncFilter(\n                periodicLogStructures.filter(\n                    (logStructure) => logStructure.logStructureGroup.__id__\n                        === logStructureGroup.__id__,\n                ),\n                async (logStructure) => this.invoke.call(this, 'reminder-check', { logStructure, todayLabel }),\n            );\n            if (!logStructures.length) {\n                return null;\n            }\n            await Promise.all(logStructures.map(async (logStructure) => {\n                logStructure.reminderScore = await this.invoke.call(this, 'reminder-score', { logStructure, todayLabel });\n            }));\n            return { ...logStructureGroup, logStructures };\n        }),\n    );\n    return reminderGroups.filter((reminderGroup) => reminderGroup);\n};\n\nfunction getSuppressUntilDate(logStructure, todayLabel) {\n    assert(logStructure.isPeriodic);\n    const todayDate = DateUtils.getDate(todayLabel);\n    const option = LogStructure.Frequency[logStructure.frequency];\n    const reminderDate = option.getNextMatch(todayDate, logStructure.frequencyArgs);\n    const warningStartDate = subDays(reminderDate, 1 + logStructure.warningDays);\n    return DateUtils.getLabel(warningStartDate);\n}\n\nActionsRegistry['reminder-complete'] = async function (input) {\n    const { logEvent: inputLogEvent, logStructure: inputLogStructure, todayLabel } = input;\n    const result = {};\n    result.logEvent = await this.invoke.call(this, 'log-event-upsert', inputLogEvent);\n    if (inputLogStructure) {\n        inputLogStructure.suppressUntilDate = getSuppressUntilDate(\n            inputLogStructure,\n            todayLabel,\n        );\n        result.logStructure = await this.invoke.call(\n            this,\n            'log-structure-upsert',\n            inputLogStructure,\n        );\n    }\n    this.broadcast('reminder-sidebar');\n    return result;\n};\n\nActionsRegistry['reminder-dismiss'] = async function (input) {\n    const { logStructure: inputLogStructure, todayLabel } = input;\n    inputLogStructure.suppressUntilDate = getSuppressUntilDate(\n        inputLogStructure,\n        todayLabel,\n    );\n    const outputLogStructure = await this.invoke.call(\n        this,\n        'log-structure-upsert',\n        inputLogStructure,\n    );\n    this.broadcast('reminder-sidebar');\n    return { logStructure: outputLogStructure };\n};\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/server/actions/settings.js",
    "content": "/* eslint-disable func-names */\n\nimport assert from 'assert';\n\nconst ActionsRegistry = {};\n\nconst INTERNAL_SETTINGS_PREFIX = '_';\n\nActionsRegistry['settings-get'] = async function () {\n    const result = {};\n    const items = await this.database.findAll('Settings');\n    items.forEach((item) => {\n        if (!item.key.startsWith(INTERNAL_SETTINGS_PREFIX)) {\n            result[item.key] = JSON.parse(item.value);\n        }\n    });\n    return result;\n};\n\nActionsRegistry['settings-set'] = async function (input) {\n    const items = await this.database.findAll('Settings', { key: Object.keys(input) });\n    const keyToItem = {};\n    items.forEach((item) => {\n        keyToItem[item.key] = item;\n    });\n    await Promise.all(Object.entries(input).map(async ([key, value]) => {\n        assert(!key.startsWith(INTERNAL_SETTINGS_PREFIX));\n        let item = keyToItem[key];\n        if (value) {\n            const fields = { key, value: JSON.stringify(value) };\n            item = await this.database.createOrUpdateItem('Settings', item, fields);\n        } else if (item) {\n            await this.database.deleteByPk('Settings', item.id);\n        }\n    }));\n    this.broadcast('settings-get');\n};\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/server/actions/suggestions.js",
    "content": "/* eslint-disable func-names */\n\nimport assert from 'assert';\n\nimport { LogKey } from '../../common/data_types';\n\nconst ActionsRegistry = {};\n\nconst getOrDefault = (item, key, defaultValue) => {\n    if (!(key in item)) item[key] = defaultValue;\n    return item[key];\n};\n\nconst getValues = (logKey) => {\n    if (!logKey.value) {\n        return [];\n    }\n    if (logKey.type === LogKey.Type.STRING_LIST) {\n        assert(Array.isArray(logKey.value));\n        return logKey.value;\n    }\n    if (logKey.type === LogKey.Type.LOG_TOPIC) {\n        return [logKey.value.__id__];\n    }\n    return [logKey.value];\n};\n\nconst buildIndex = (items, getLogKeys) => {\n    if (!items.length) {\n        return null;\n    }\n    const indexData = {};\n    items.forEach((item) => {\n        getLogKeys(item).forEach((logKey, index) => {\n            const keyIndexData = getOrDefault(indexData, index, { logKey, counts: {} });\n            getValues(logKey).forEach((value) => {\n                getOrDefault(keyIndexData.counts, value, 0);\n                keyIndexData.counts[value] += 1;\n            });\n        });\n    });\n    Object.values(indexData).forEach((keyIndexData) => {\n        keyIndexData.values = Array.from(Object.entries(keyIndexData.counts))\n            .sort((left, right) => left[1] - right[1])\n            .map((pair) => pair[0]);\n        delete keyIndexData.counts;\n    });\n    return indexData;\n};\n\nconst lookupIndex = (indexData, index, query) => {\n    if (!indexData) return [];\n    const keyIndexData = indexData[index];\n    if (!keyIndexData) return [];\n    return keyIndexData.values.filter((value) => value.startsWith(query));\n};\n\nActionsRegistry['structure-value-typeahead-index-$cached'] = async function (input) {\n    const where = { logStructure: { __id__: input.structure_id } };\n    const logEvents = await this.invoke.call(this, 'log-event-list', { where });\n    return buildIndex(logEvents, (logEvent) => logEvent.logStructure.eventKeys);\n};\n\nActionsRegistry['topic-value-typeahead-index-$cached'] = async function (input) {\n    const where = { parentLogTopic: { __id__: input.parent_topic_id } };\n    const childLogTopics = await this.invoke.call(this, 'log-topic-list', { where });\n    return buildIndex(childLogTopics, (childLogTopic) => childLogTopic.parentLogTopic.childKeys);\n};\n\nActionsRegistry['value-typeahead'] = async function (input) {\n    if (!input.source) {\n        return [];\n    } if (input.source.__type__ === 'log-structure') {\n        const structureValueTypeaheadIndex = await this.invoke.call(\n            this,\n            'structure-value-typeahead-index',\n            { structure_id: input.source.__id__ },\n        );\n        return lookupIndex(structureValueTypeaheadIndex, input.index, input.query);\n    } if (input.source.__type__ === 'log-topic') {\n        const topicValueTypeaheadIndex = await this.invoke.call(\n            this,\n            'topic-value-typeahead-index',\n            { parent_topic_id: input.source.__id__ },\n        );\n        return lookupIndex(topicValueTypeaheadIndex, input.index, input.query);\n    }\n    throw new Error(`unsupported source: ${input.source.__type__}`);\n};\n\nexport default ActionsRegistry;\n"
  },
  {
    "path": "src/server/actions.js",
    "content": "/* eslint-disable func-names */\n/* eslint-disable max-classes-per-file */\n\nimport assert from 'assert';\n\n// This method only exists within webpack, so we need to provide it for Jest.\n// Note: babel-plugin-transform-require-context does not work for Jest.\n// Source = https://stackoverflow.com/a/42191018/903585\nif (typeof require.context === 'undefined') {\n    // eslint-disable-next-line global-require\n    const fs = require('fs');\n    // eslint-disable-next-line global-require\n    const path = require('path');\n    require.context = (base = '.', scanSubDirectories = false, regularExpression = /\\.js$/) => {\n        const files = {};\n        function readDirectory(directory) {\n            fs.readdirSync(directory).forEach((file) => {\n                const fullPath = path.resolve(directory, file);\n                if (fs.statSync(fullPath).isDirectory()) {\n                    if (scanSubDirectories) readDirectory(fullPath);\n                    return;\n                }\n                if (!regularExpression.test(fullPath)) return;\n                files[fullPath] = true;\n            });\n        }\n        readDirectory(path.resolve(__dirname, base));\n        function Module(file) {\n            // eslint-disable-next-line import/no-dynamic-require, global-require\n            return require(file);\n        }\n        Module.keys = () => Object.keys(files);\n        return Module;\n    };\n}\n\nclass ActionsRegistry {\n    static get(configPlugins) {\n        if (ActionsRegistry.result) {\n            return ActionsRegistry.result;\n        }\n        const result = {};\n\n        const actionsContext = require.context('./actions', false, /\\.js$/);\n        actionsContext.keys()\n            .forEach((filePath) => {\n                const exports = actionsContext(filePath);\n                ActionsRegistry.build(result, exports.default);\n            });\n\n        if (Array.isArray(configPlugins)) {\n            const pluginsContext = require.context('../plugins', true, /actions\\.js$/);\n            const pluginPatterns = configPlugins.map((pattern) => new RegExp(pattern));\n            pluginsContext.keys()\n                .filter((filePath) => pluginPatterns.some((regex) => filePath.match(regex)))\n                .forEach((filePath) => {\n                    const exports = pluginsContext(filePath);\n                    ActionsRegistry.build(result, exports.default);\n                });\n        }\n\n        ActionsRegistry.result = result;\n        return result;\n    }\n\n    static build(result, nameToMethods) {\n        Object.entries(nameToMethods).forEach(([name, method]) => {\n            const cacheSuffix = '-$cached';\n            if (name.endsWith(cacheSuffix)) {\n                ActionsRegistry.useCache(result, name.slice(0, -cacheSuffix.length), method);\n            } else {\n                result[name] = method;\n            }\n        });\n    }\n\n    static useCache(result, name, method) {\n        const actualName = `${name}-actual`;\n        result[actualName] = method;\n        result[name] = async function (input = null) {\n            const serializedInput = JSON.stringify(input);\n            if (!(name in this.memory)) {\n                this.memory[name] = {};\n            }\n            if (!(serializedInput in this.memory[name])) {\n                this.memory[name][serializedInput] = new Promise((resolve, reject) => {\n                    this.invoke.call(this, actualName, input).then(resolve).catch(reject);\n                });\n            }\n            const promise = this.memory[name][serializedInput];\n            assert(promise);\n            return promise;\n        };\n        result[`${name}-$refresh`] = async function (input = null) {\n            const serializedInput = JSON.stringify(input);\n            if (name in this.memory) {\n                if (serializedInput in this.memory[name]) {\n                    delete this.memory[name][serializedInput];\n                }\n            }\n        };\n    }\n}\n\nexport default class {\n    constructor(config, database) {\n        this.config = config;\n        this.database = database;\n        this.registry = ActionsRegistry.get(config ? config.plugins : null);\n        this.memory = {};\n        this.socket = null;\n        this.broadcasts = null;\n    }\n\n    registerBroadcast(socket) {\n        this.socket = socket;\n    }\n\n    getBroadcasts() { // used for tests\n        const result = this.broadcasts;\n        this.broadcasts = null;\n        return result;\n    }\n\n    // eslint-disable-next-line class-methods-use-this\n    has(name) {\n        return name in this.registry;\n    }\n\n    async invoke(name, input, moreContext = {}) {\n        const context = {\n            ...moreContext,\n            invoke(innerName, innerInput) {\n                if (!(innerName in this.registry)) {\n                    throw new Error(`unknown action: ${innerName}`);\n                }\n                try {\n                    return this.registry[innerName].call(context, innerInput);\n                } catch (error) {\n                    const serializedInput = JSON.stringify(input, null, 4);\n                    throw new Error(`${innerName}: ${serializedInput}\\n\\n${error.message}`);\n                }\n            },\n            config: this.config,\n            // The Object.create method creates a new object with the given prototype.\n            // This allows us to concurrently set the transaction field below.\n            database: Object.create(this.database),\n            // The `registry` is used from the `invoke` method above.\n            registry: this.registry,\n            // The `memory` object is shared across all actions, used for caching.\n            memory: this.memory,\n            // Arguments for deferred `invoke` operations on separate transactions.\n            deferredInvoke: [],\n            // Transmit logs to client.\n            console: {},\n        };\n        ['info', 'log', 'warning', 'error'].forEach((logLevel) => {\n            context.console[logLevel] = (...args) => {\n                if (this.socket) {\n                    this.socket.log(logLevel, ...args);\n                }\n            };\n        });\n        context.database.transaction = await this.database.sequelize.transaction();\n        try {\n            const broadcasts = [];\n            context.broadcast = (...args) => broadcasts.push(args);\n            const response = await context.invoke.call(context, name, input); // action\n            await context.database.transaction.commit();\n            if (this.socket) {\n                broadcasts.forEach((args) => this.socket.broadcast(...args));\n            } else {\n                this.broadcasts = broadcasts;\n            }\n            context.deferredInvoke.forEach((deferredArgs) => this.invoke(...deferredArgs));\n            return response;\n        } catch (error) {\n            // console.error(error.toString());\n            try {\n                await context.database.transaction.rollback();\n            } catch (anotherError) {\n                throw error;\n            }\n            throw error;\n        }\n    }\n}\n"
  },
  {
    "path": "src/server/database.js",
    "content": "import assert from 'assert';\nimport fs from 'fs';\n\nimport { isRealItem } from '../common/data_types';\nimport { getDataModels } from './models';\n\nconst Sequelize = require('sequelize');\n\nexport default class {\n    constructor(config) {\n        this.config = config;\n        this.sequelize = new Sequelize(this.config);\n        const nameAndModels = getDataModels(this.sequelize);\n        this._modelSequence = nameAndModels.map(([_name, model]) => model);\n        this._models = nameAndModels.reduce((result, [name, model]) => {\n            result[name] = model;\n            return result;\n        }, {});\n        this.Op = Sequelize.Op;\n        this.transaction = null;\n    }\n\n    getTransaction() {\n        // The this.transaction field is set by the Actions class.\n        // By creating a new object with the database instance as a prototype,\n        // we have the transaction available in context, and API remains simple.\n        assert(!!this.transaction);\n        return this.transaction;\n    }\n\n    async reset() {\n        // You cant invoke sync during an active transaction!\n        await this.transaction.commit();\n        if (fs.existsSync(this.sequelize.options.storage)) {\n            fs.unlinkSync(this.sequelize.options.storage);\n        }\n        if (this.config.dialect === 'sqlite') {\n            // https://github.com/sequelize/sequelize/issues/11583\n            await this.sequelize.query('PRAGMA foreign_keys = false;');\n            await this.sequelize.sync({ force: true });\n            await this.sequelize.query('PRAGMA foreign_keys = true;');\n        } else {\n            await this.sequelize.sync({ force: true });\n        }\n        this.transaction = await this.sequelize.transaction();\n    }\n\n    async close() {\n        await this.sequelize.close();\n    }\n\n    getModelSequence() {\n        return this._modelSequence;\n    }\n\n    async build(name) {\n        const Model = this._models[name];\n        return Model.build({});\n    }\n\n    async create(name, fields) {\n        const transaction = this.getTransaction();\n        const { __id__: _id, ...remainingFields } = fields;\n        const Model = this._models[name];\n        return Model.create(\n            remainingFields,\n            // Why specify fields? https://github.com/sequelize/sequelize/issues/11417\n            { fields: Object.keys(remainingFields), transaction },\n        );\n    }\n\n    async update(name, fields) {\n        const transaction = this.getTransaction();\n        const { id, ...remainingFields } = fields;\n        const Model = this._models[name];\n        const instance = await Model.findByPk(id, { transaction });\n        return instance.update(remainingFields, { transaction });\n    }\n\n    async createOrUpdateItem(name, item, fields) {\n        const transaction = this.getTransaction();\n        if (item) {\n            return item.update(fields, { transaction });\n        }\n        return this.create(name, fields);\n    }\n\n    async findAll(name, where, order, limit) {\n        const transaction = this.getTransaction();\n        const Model = this._models[name];\n        return Model.findAll({\n            where, order, limit, transaction,\n        });\n    }\n\n    async findOne(name, where, order) {\n        const transaction = this.getTransaction();\n        const Model = this._models[name];\n        return Model.findOne({ where, order, transaction });\n    }\n\n    async findByPk(name, id) {\n        const transaction = this.getTransaction();\n        const Model = this._models[name];\n        return Model.findByPk(id, { transaction });\n    }\n\n    async findItem(name, item) {\n        if (isRealItem(item)) {\n            return this.findByPk(name, item.__id__);\n        }\n        return null;\n    }\n\n    async count(name, where, group) {\n        const transaction = this.getTransaction();\n        const Model = this._models[name];\n        return Model.count({ where, group, transaction });\n    }\n\n    async createOrFind(name, where, updateFields) {\n        const transaction = this.getTransaction();\n        const Model = this._models[name];\n        const instance = await Model.findOne({ where, transaction });\n        if (!instance) {\n            return this.create(name, { ...where, ...updateFields });\n        }\n        return instance;\n    }\n\n    async deleteAll(name, where) {\n        const transaction = this.getTransaction();\n        const Model = this._models[name];\n        return Model.destroy({ where, transaction });\n    }\n\n    async deleteByPk(name, id) {\n        const transaction = this.getTransaction();\n        const Model = this._models[name];\n        const instance = await Model.findByPk(id, { transaction });\n        return instance.destroy({ transaction });\n    }\n\n    async getEdges(edgeName, leftName, leftId) {\n        const transaction = this.getTransaction();\n        const EdgeModel = this._models[edgeName];\n        const edges = await EdgeModel.findAll({\n            where: { [leftName]: leftId },\n            transaction,\n        });\n        if (edges.length > 1 && typeof edges[0].ordering_index !== 'undefined') {\n            edges.sort((left, right) => left.ordering_index - right.ordering_index);\n        }\n        return edges;\n    }\n\n    async getNodesByEdge(edgeName, leftName, leftId, rightName, rightType) {\n        const transaction = this.getTransaction();\n        const edges = await this.getEdges(edgeName, leftName, leftId);\n        const NodeModel = this._models[rightType];\n        const nodes = await Promise.all(\n            edges.map((edge) => NodeModel.findByPk(edge[rightName], { transaction })),\n        );\n        return nodes;\n    }\n\n    async setEdges(edgeName, leftName, leftId, rightName, right) {\n        const transaction = this.getTransaction();\n        const Model = this._models[edgeName];\n        const existingEdges = await Model.findAll({ where: { [leftName]: leftId }, transaction });\n        const existingIDs = existingEdges.map((edge) => edge[rightName].toString());\n        // Why specify fields? https://github.com/sequelize/sequelize/issues/11417\n        const fields = [\n            leftName,\n            rightName,\n            ...Object.keys(Object.values(right)[0] || {}),\n        ];\n        // eslint-disable-next-line no-unused-vars\n        const [updatedEdges, deletedEdges] = await Promise.all([\n            Promise.all(\n                existingEdges\n                    .filter((edge) => edge[rightName] in right)\n                    .map((edge) => edge.update(right[edge[rightName]], { transaction })),\n            ),\n            Promise.all(\n                existingEdges\n                    .filter((edge) => !(edge[rightName] in right))\n                    .map((edge) => edge.destroy({ transaction })),\n            ),\n        ]);\n        // eslint-disable-next-line no-unused-vars\n        const createdEdges = await Promise.all(\n            Object.keys(right)\n                .filter((rightId) => !existingIDs.includes(rightId))\n                .map((rightId) => Model.create({\n                    [leftName]: leftId,\n                    [rightName]: rightId,\n                    ...right[rightId],\n                }, { fields, transaction })),\n        );\n        return deletedEdges;\n    }\n}\n"
  },
  {
    "path": "src/server/index.js",
    "content": "/* eslint-disable no-console */\n\nimport '../common/polyfill';\n\nimport express from 'express';\nimport fs from 'fs';\nimport http from 'http';\nimport process from 'process';\nimport SingleInstance from 'single-instance';\nimport SocketIO from 'socket.io';\nimport yargs from 'yargs';\n\nimport SocketRPC from '../common/SocketRPC';\nimport Actions from './actions';\nimport Database from './database';\n\nasync function init() {\n    this.database = new Database(this.config.database);\n    this.actions = new Actions(this.config, this.database);\n}\n\nasync function startServer() {\n    await this.actions.invoke('database-validate', { verbose: true });\n    const app = express();\n    const server = http.Server(app);\n    const io = SocketIO(server);\n    io.on('connection', (socket) => SocketRPC.server(socket, this.actions));\n    const { host, port } = this.config.server;\n    app.get('/', (req, res) => {\n        res.cookie('host', host);\n        res.cookie('port', port);\n        res.cookie('plugins', JSON.stringify(this.config.plugins || []));\n        res.sendFile('index.html', { root: 'dist' });\n    });\n    app.use(express.static('dist'));\n    this.server = server.listen(port, host);\n    console.info(`Server running at http://${host}:${port}`);\n}\n\nasync function cleanup() {\n    if (this.server) {\n        this.server.close();\n    }\n    if (this.database) {\n        this.database.close();\n    }\n}\n\nasync function main(argv) {\n    if (this === global) {\n        main.call({}, argv);\n        return;\n    }\n\n    this.config = JSON.parse(fs.readFileSync(argv.configPath));\n\n    const locker = new SingleInstance(this.config.lock_name || 'glados');\n    console.info('Acquiring lock ...');\n    await locker.lock();\n    console.info('Acquired lock!');\n\n    await init.call(this);\n    if (argv.action) {\n        await this.actions.invoke(argv.action, { verbose: true });\n    } else {\n        await startServer.call(this);\n        // Let the server run until we get a signal.\n        await new Promise((resolve) => {\n            process.on('SIGTERM', resolve);\n            process.on('SIGINT', resolve);\n        });\n    }\n    await cleanup.call(this);\n\n    console.info('Releasing lock ...');\n    await locker.unlock();\n    console.info('Released lock!');\n}\n\n// Put everything together!\n\nconst { argv } = yargs\n    .option('configPath', { alias: 'c', default: 'config.json' })\n    .demandOption('configPath')\n    .option('action', { alias: 'a' })\n    .choices('action', ['database-reset', 'backup-load', 'backup-save']);\n\nmain(argv).catch((error) => console.error(error));\n"
  },
  {
    "path": "src/server/models.js",
    "content": "const Sequelize = require('sequelize');\n\nexport function getDataFormatVersion() {\n    // This value is used to ensure that the backup file being loaded\n    // is still compatible with this version of code.\n    // In case of database schema changes, this value should be bumped,\n    // and a script can be written to generate a new backup file from an older version.\n    return '100';\n}\n\nexport function getDataModels(sequelize) {\n    const options = {\n        timestamps: false,\n        underscored: true,\n    };\n\n    const Settings = sequelize.define(\n        'settings',\n        {\n            id: {\n                type: Sequelize.INTEGER,\n                autoIncrement: true,\n                primaryKey: true,\n            },\n            key: {\n                type: Sequelize.STRING,\n                allowNull: false,\n            },\n            value: {\n                type: Sequelize.STRING,\n                allowNull: false,\n            },\n        },\n        {\n            ...options,\n            indexes: [\n                { unique: true, fields: ['key'] },\n            ],\n        },\n    );\n\n    const LogTopic = sequelize.define(\n        'log_topics',\n        {\n            id: {\n                type: Sequelize.INTEGER,\n                autoIncrement: true,\n                primaryKey: true,\n            },\n            parent_topic_id: {\n                type: Sequelize.INTEGER,\n                allowNull: true,\n            },\n            ordering_index: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            name: {\n                type: Sequelize.STRING,\n                allowNull: false,\n            },\n            details: {\n                type: Sequelize.TEXT,\n                allowNull: false,\n            },\n            child_count: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            is_favorite: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n            is_deprecated: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n            // Keys & Values\n            child_keys: {\n                type: Sequelize.TEXT,\n                allowNull: true,\n            },\n            child_name_template: {\n                type: Sequelize.TEXT,\n                allowNull: true,\n            },\n            parent_values: {\n                type: Sequelize.TEXT,\n                allowNull: true,\n            },\n        },\n        {\n            ...options,\n            indexes: [\n                { unique: true, fields: ['name'] },\n            ],\n        },\n    );\n\n    LogTopic.belongsTo(LogTopic, {\n        foreignKey: 'parent_topic_id',\n        allowNull: true,\n        onDelete: 'restrict',\n        onUpdate: 'restrict',\n    });\n\n    const LogTopicToLogTopic = sequelize.define(\n        'log_topics_to_log_topics',\n        {\n            source_topic_id: {\n                type: Sequelize.INTEGER,\n                references: {\n                    model: LogTopic,\n                    key: 'id',\n                },\n            },\n            target_topic_id: {\n                type: Sequelize.INTEGER,\n                references: {\n                    model: LogTopic,\n                    key: 'id',\n                },\n            },\n        },\n        options,\n    );\n\n    LogTopic.belongsToMany(LogTopic, {\n        as: 'Source',\n        through: LogTopicToLogTopic,\n        foreignKey: 'source_topic_id',\n        // Deleteing a source topic is allowed!\n        // The links will be broken, and the Tags could be cleaned up.\n        onDelete: 'cascade',\n        onUpdate: 'cascade',\n    });\n\n    LogTopic.belongsToMany(LogTopic, {\n        as: 'Target',\n        through: LogTopicToLogTopic,\n        foreignKey: 'target_topic_id',\n        onDelete: 'restrict',\n        onUpdate: 'restrict',\n    });\n\n    const LogStructureGroup = sequelize.define(\n        'log_structure_groups',\n        {\n            id: {\n                type: Sequelize.INTEGER,\n                autoIncrement: true,\n                primaryKey: true,\n            },\n            ordering_index: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            name: {\n                type: Sequelize.STRING,\n                allowNull: false,\n            },\n        },\n        options,\n    );\n\n    const LogStructure = sequelize.define(\n        'log_structures',\n        {\n            id: {\n                type: Sequelize.INTEGER,\n                autoIncrement: true,\n                primaryKey: true,\n            },\n            group_id: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            ordering_index: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            name: {\n                type: Sequelize.STRING,\n                allowNull: false,\n            },\n            details: {\n                type: Sequelize.TEXT,\n                allowNull: false,\n            },\n            // Should this structure have key-value-pairs?\n            event_keys: {\n                type: Sequelize.TEXT,\n                allowNull: false,\n            },\n            event_title_template: {\n                type: Sequelize.TEXT,\n                allowNull: false,\n            },\n            event_needs_edit: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n            event_allow_details: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n            // Should this structure have reminders?\n            is_periodic: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n            reminder_text: {\n                type: Sequelize.STRING,\n                allowNull: true,\n            },\n            frequency: {\n                type: Sequelize.STRING,\n                allowNull: true,\n            },\n            frequency_args: {\n                type: Sequelize.STRING,\n                allowNull: true,\n            },\n            warning_days: {\n                type: Sequelize.INTEGER,\n                allowNull: true,\n            },\n            suppress_until_date: {\n                type: Sequelize.STRING,\n                allowNull: true,\n            },\n            // Additional fields to be copied to events.\n            log_level: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            is_favorite: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n            is_deprecated: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n        },\n        options,\n    );\n\n    LogStructure.belongsTo(LogStructureGroup, {\n        foreignKey: 'group_id',\n        allowNull: false,\n        onDelete: 'restrict',\n        onUpdate: 'restrict',\n    });\n\n    const LogStructureToLogTopic = sequelize.define(\n        'log_structures_to_log_topics',\n        {\n            source_structure_id: {\n                type: Sequelize.INTEGER,\n                references: {\n                    model: LogStructure,\n                    key: 'id',\n                },\n            },\n            target_topic_id: {\n                type: Sequelize.INTEGER,\n                references: {\n                    model: LogTopic,\n                    key: 'id',\n                },\n            },\n        },\n        options,\n    );\n\n    LogStructure.belongsToMany(LogTopic, {\n        through: LogStructureToLogTopic,\n        foreignKey: 'source_structure_id',\n        // Deleteing an structure is allowed!\n        // The links will be broken, and the Tags could be cleaned up.\n        onDelete: 'cascade',\n        onUpdate: 'cascade',\n    });\n\n    LogTopic.belongsToMany(LogStructure, {\n        through: LogStructureToLogTopic,\n        foreignKey: 'target_topic_id',\n        onDelete: 'restrict',\n        onUpdate: 'restrict',\n    });\n\n    // Estimated scale? 50 events per day * 365 days * 10 years = 182,500 events\n    // Size of 1 event? 1 kb, so total size over 10 years ~= 200mb\n    const LogEvent = sequelize.define(\n        'log_events',\n        {\n            id: {\n                type: Sequelize.INTEGER,\n                autoIncrement: true,\n                primaryKey: true,\n            },\n            date: {\n                type: Sequelize.STRING,\n                allowNull: true,\n            },\n            ordering_index: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            title: {\n                type: Sequelize.TEXT,\n                allowNull: false,\n            },\n            structure_id: {\n                type: Sequelize.INTEGER,\n                allowNull: true,\n            },\n            structure_values: {\n                type: Sequelize.TEXT,\n                allowNull: true,\n            },\n            details: {\n                type: Sequelize.TEXT,\n                allowNull: false,\n            },\n            log_level: {\n                type: Sequelize.INTEGER,\n                allowNull: false,\n            },\n            is_favorite: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n            is_complete: {\n                type: Sequelize.BOOLEAN,\n                allowNull: false,\n            },\n        },\n        options,\n    );\n\n    LogEvent.belongsTo(LogStructure, {\n        foreignKey: 'structure_id',\n        allowNull: true,\n        onDelete: 'restrict',\n        onUpdate: 'restrict',\n    });\n\n    const LogEventToLogTopic = sequelize.define(\n        'log_events_to_log_topics',\n        {\n            source_event_id: {\n                type: Sequelize.INTEGER,\n                references: {\n                    model: LogEvent,\n                    key: 'id',\n                },\n            },\n            target_topic_id: {\n                type: Sequelize.INTEGER,\n                references: {\n                    model: LogTopic,\n                    key: 'id',\n                },\n            },\n        },\n        options,\n    );\n\n    LogEvent.belongsToMany(LogTopic, {\n        through: LogEventToLogTopic,\n        foreignKey: 'source_event_id',\n        // Deleteing an event is allowed!\n        // The links will be broken, and the Tags could be cleaned up.\n        onDelete: 'cascade',\n        onUpdate: 'cascade',\n    });\n\n    LogTopic.belongsToMany(LogEvent, {\n        through: LogEventToLogTopic,\n        foreignKey: 'target_topic_id',\n        onDelete: 'restrict',\n        onUpdate: 'restrict',\n    });\n\n    // The following sequence of models is used to load data from backups\n    // while respecting foreign key constraints.\n    return [\n        ['Settings', Settings],\n        ['LogTopic', LogTopic],\n        ['LogTopicToLogTopic', LogTopicToLogTopic],\n        ['LogStructureGroup', LogStructureGroup],\n        ['LogStructure', LogStructure],\n        ['LogStructureToLogTopic', LogStructureToLogTopic],\n        ['LogEvent', LogEvent],\n        ['LogEventToLogTopic', LogEventToLogTopic],\n    ];\n}\n"
  }
]