├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── branch-naming.yml │ ├── ci-pr.yml │ └── ci.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode └── launch.json ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── lwc.config.json ├── package-lock.json ├── package.json ├── scripts ├── .eslintrc ├── plugin-html.js └── rollup.config.js └── src ├── index.html ├── index.js ├── modules ├── domain │ └── lwcBuilderEvent │ │ └── lwcBuilderEvent.js ├── jsconfig.json └── my │ ├── app │ ├── __tests__ │ │ └── app.test.js │ ├── app.css │ ├── app.html │ └── app.js │ ├── buildContents │ ├── __tests__ │ │ ├── buildContents.test.js │ │ ├── buildCss.test.js │ │ ├── buildHtml.test.js │ │ ├── buildJs.test.js │ │ ├── buildMeta.test.js │ │ ├── buildSvg.test.js │ │ └── buildTest.test.js │ ├── buildContents.js │ ├── buildCss.js │ ├── buildHtml.js │ ├── buildJs.js │ ├── buildMeta.js │ ├── buildSvg.js │ └── buildTest.js │ ├── footer │ ├── footer.css │ ├── footer.html │ └── footer.js │ ├── form │ ├── __tests__ │ │ └── form.test.js │ ├── form.css │ ├── form.html │ └── form.js │ ├── preview │ ├── __tests__ │ │ └── preview.test.js │ ├── preview.css │ ├── preview.html │ └── preview.js │ ├── previewContent │ ├── __tests__ │ │ └── previewContent.test.js │ ├── previewContent.css │ ├── previewContent.html │ └── previewContent.js │ ├── previewHeader │ ├── __tests__ │ │ └── previewHeader.test.js │ ├── previewHeader.css │ ├── previewHeader.html │ └── previewHeader.js │ ├── propertyCmsFilterItem │ ├── propertyCmsFilterItem.css │ ├── propertyCmsFilterItem.html │ └── propertyCmsFilterItem.js │ ├── propertyDefinition │ ├── propertyDefinition.css │ ├── propertyDefinition.html │ └── propertyDefinition.js │ ├── propertyTargetItem │ ├── propertyTargetItem.html │ └── propertyTargetItem.js │ ├── sobjectDefinition │ ├── sobjectDefinition.css │ ├── sobjectDefinition.html │ └── sobjectDefinition.js │ ├── svgUploader │ ├── svgUploader.css │ ├── svgUploader.html │ └── svgUploader.js │ └── targetDefinition │ ├── targetDefinition.css │ ├── targetDefinition.html │ └── targetDefinition.js └── resources ├── favicon.ico ├── lwc.png └── slds.min.css /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "commonjs": true, 5 | "browser": true, 6 | "es6": true 7 | }, 8 | "plugins": ["inclusive-language", "jsdoc", "eslint-plugin-header"], 9 | "extends": [ 10 | "@salesforce/eslint-config-lwc/recommended", 11 | "plugin:prettier/recommended" 12 | ], 13 | "rules": { 14 | "@lwc/lwc/no-async-operation": "off", 15 | "@lwc/lwc/no-inner-html": "warn", 16 | "@lwc/lwc/no-document-query": "warn", 17 | "inclusive-language/use-inclusive-words": [ 18 | "error", 19 | { 20 | "allowedTerms": [ 21 | { "term": "masterlabel", "allowPartialMatches": true }, 22 | { "term": "master-label", "allowPartialMatches": true } 23 | ] 24 | } 25 | ], 26 | "header/header": [ 27 | 2, 28 | "block", 29 | [ 30 | "", 31 | { 32 | "pattern": " \\* Copyright \\(c\\) \\d{4}, salesforce\\.com, inc\\.", 33 | "template": " * Copyright (c) 2021, salesforce.com, inc." 34 | }, 35 | " * All rights reserved.", 36 | " * Licensed under the BSD 3-Clause license.", 37 | " * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause", 38 | " " 39 | ] 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/branch-naming.yml: -------------------------------------------------------------------------------- 1 | # Unique name for this workflow 2 | name: Enforce branch naming 3 | 4 | # Definition when the workflow should run 5 | on: 6 | push: 7 | branches-ignore: 8 | - main 9 | 10 | # Jobs to be executed 11 | jobs: 12 | validate-branch-name: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Validate branch name requirements 16 | uses: deepakputhraya/action-branch-name@master 17 | with: 18 | regex: '([a-zA-Z])+\/([a-zA-Z])+' 19 | min_length: 6 20 | -------------------------------------------------------------------------------- /.github/workflows/ci-pr.yml: -------------------------------------------------------------------------------- 1 | # Unique name for this workflow 2 | name: CI (PR) 3 | 4 | # Definition when the workflow should run 5 | on: 6 | pull_request: 7 | types: [opened, edited, synchronize, reopened] 8 | 9 | # Jobs to be executed 10 | jobs: 11 | format-lint-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | # Checkout the code in the pull request 15 | - name: 'Checkout source code' 16 | uses: actions/checkout@v2 17 | 18 | # Cache node_modules to speed up the process 19 | - name: Restore npm cache 20 | id: npm-cache 21 | uses: actions/cache@v2 22 | with: 23 | path: '**/node_modules' 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-v1 25 | 26 | # Install npm dependencies 27 | - name: 'Install dependencies if needed' 28 | if: steps.npm-cache.outputs.cache-hit != 'true' 29 | run: npm ci 30 | 31 | # Prettier formatting 32 | - name: 'Code formatting verification with Prettier' 33 | run: npm run prettier:verify 34 | 35 | # ESlint 36 | - name: 'Lint JavaScript' 37 | run: npm run lint 38 | 39 | # Unit tests 40 | - name: 'Run unit tests' 41 | run: npm run test:unit:coverage 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Unique name for this workflow 2 | name: CI 3 | 4 | # Definition when the workflow should run 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | # Jobs to be executed 11 | jobs: 12 | format-lint-test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # Checkout the code in the pull request 16 | - name: 'Checkout source code' 17 | uses: actions/checkout@v2 18 | 19 | # Cache node_modules to speed up the process 20 | - name: Restore npm cache 21 | id: npm-cache 22 | uses: actions/cache@v2 23 | with: 24 | path: '**/node_modules' 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-v1 26 | 27 | # Install npm dependencies 28 | - name: 'Install dependencies if needed' 29 | if: steps.npm-cache.outputs.cache-hit != 'true' 30 | run: npm ci 31 | 32 | # Prettier formatting 33 | - name: 'Code formatting verification with Prettier' 34 | run: npm run prettier:verify 35 | 36 | # ESlint 37 | - name: 'Lint JavaScript' 38 | run: npm run lint 39 | 40 | # Unit tests 41 | - name: 'Run unit tests' 42 | run: npm run test:unit:coverage 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Log files 2 | logs 3 | *.log 4 | *-debug.log 5 | *-error.log 6 | 7 | # Standard lib folder 8 | /lib 9 | 10 | # Standard dist folder 11 | /dist 12 | 13 | # Tooling files 14 | node_modules 15 | 16 | # Temp directory 17 | /tmp 18 | 19 | # Jest coverage folder 20 | /coverage 21 | 22 | # MacOS system files 23 | .DS_Store 24 | 25 | # Windows system files 26 | Thumbs.db 27 | ehthumbs.db 28 | [Dd]esktop.ini 29 | $RECYCLE.BIN/ 30 | 31 | .sfdx/* 32 | 33 | .eslintcache -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint && npm run prettier && npm run test:unit 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Standard src folder 2 | /src 3 | 4 | # Tooling files 5 | node_modules 6 | .eslintignore 7 | .eslintrc.json 8 | .prettierignore 9 | .prettierrc 10 | lwc.config.json 11 | 12 | # MacOS system files 13 | .DS_Store -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Jest coverage 2 | coverage/ 3 | 4 | # Default lib folder 5 | lib/ 6 | 7 | # Default dist folder 8 | dist/ 9 | 10 | # Default resources folder 11 | src/resources -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "overrides": [ 6 | { 7 | "files": "**/*.html", 8 | "options": { "parser": "lwc" } 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch & Attach For Jest Tests", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeArgs": [ 12 | "--inspect-brk", 13 | "${workspaceRoot}/node_modules/.bin/jest", 14 | "--runInBand" 15 | ], 16 | "console": "integratedTerminal", 17 | "internalConsoleOptions": "neverOpen", 18 | "port": 9229 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "lwc-builder-ui" package will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [1.0.0] - 2024-07-31 8 | 9 | - Support Summer \'24 v61.0 api version 10 | 11 | ## [0.1.29] - 2023-10-27 12 | 13 | - Support Winter \'24 v59.0 api version 14 | 15 | ## [0.1.26] - 2023-3-10 16 | 17 | - Support Spring \'23 v57.0 api version 18 | 19 | ## [0.1.25] - 2022-10-26 20 | 21 | - Add Salesforce Winter'23 v56.0 version. 22 | - Add Salesforce Summer'22 v55.0 version. 23 | - Add lightningStatic\_\_Email support 24 | - Add analytics\_\_Dashboard 25 | - Add Responsive property attribute for lightningCommunity\_\_Default 26 | - Fix bug property bug on target uncheck 27 | - Fix wrong base class names for snapins targets 28 | - Fix preview not updated after property removal 29 | - Change property name after @api from break line to a whitespace. 30 | 31 | ## [0.1.22] - 2021-08-21 32 | 33 | Salesforce Summer'21 v52.0 support. 34 | 35 | ### Added 36 | 37 | - [Quick Actions](https://help.salesforce.com/articleView?id=release-notes.rn_lwc_quick_actions.htm&type=5&release=232) target support. [LWC Doc](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.use_quick_actions) 38 | - lightningCommunity**Page_Layout, lightningCommunity**Theme_Layout 39 | - ContentReference property type + filter attribute 40 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | - Using welcoming and inclusive language 39 | - Being respectful of differing viewpoints and experiences 40 | - Gracefully accepting constructive criticism 41 | - Focusing on what is best for the community 42 | - Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | - The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | - Personal attacks, insulting/derogatory comments, or trolling 49 | - Public or private harassment 50 | - Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | - Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | - Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org 'https://www.contributor-covenant.org/' 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | 1. Familiarize yourself with the codebase by reading the [docs](docs), in 4 | particular the [development](contributing/developing.md) doc. 5 | 1. Create a new issue before starting your project so that we can keep track of 6 | what you are trying to add/fix. That way, we can also offer suggestions or 7 | let you know if there is already an effort in progress. 8 | 1. Fork this repository. 9 | 1. The [README](README.md) has details on how to set up your environment. 10 | 1. Create a _topic_ branch in your fork based on the correct branch (usually the **develop** branch, see [Branches section](#branches) below). Note, this step is recommended but technically not required if contributing using a fork. 11 | 1. Edit the code in your fork. 12 | 1. Sign CLA (see [CLA](#cla) below) 13 | 1. Send us a pull request when you are done. We'll review your code, suggest any 14 | needed changes, and merge it in. 15 | 16 | ### Committing 17 | 18 | 1. We enforce commit message format. We recommend using [commitizen](https://github.com/commitizen/cz-cli) by installing it with `npm install -g commitizen` and running `npm run commit-init`. When you commit, we recommend that you use `npm run commit`, which prompts you with a series of questions to format the commit message. Or you can use our VS Code Task `Commit`. 19 | 1. The commit message format that we expect is: `type: commit message`. Valid types are: feat, fix, improvement, docs, style, refactor, perf, test, build, ci, chore and revert. 20 | 1. Before commit and push, Husky runs several hooks to ensure the commit message is in the correct format and that everything lints and compiles properly. 21 | 22 | ### CLA 23 | 24 | External contributors will be required to sign a Contributor's License 25 | Agreement. You can do so by going to https://cla.salesforce.com/sign-cla. 26 | 27 | ## Branches 28 | 29 | - We work in `develop`. 30 | - Our released (aka. _production_) branch is `main`. 31 | - Our work happens in _topic_ branches (feature and/or bug-fix). 32 | - feature as well as bug-fix branches are based on `develop` 33 | - branches _should_ be kept up-to-date using `rebase` 34 | - see below for further merge instructions 35 | 36 | ### Merging between branches 37 | 38 | - We try to limit merge commits as much as possible. 39 | 40 | - They are usually only ok when done by our release automation. 41 | 42 | - _Topic_ branches are: 43 | 44 | 1. based on `develop` and will be 45 | 1. squash-merged into `develop`. 46 | 47 | - Hot-fix branches are an exception. 48 | - Instead we aim for faster cycles and a generally stable `develop` branch. 49 | 50 | ### Merging `develop` into `main` 51 | 52 | - When a development cycle finishes, the content of the `develop` branch becomes the `main` branch. 53 | 54 | ``` 55 | $ git checkout main 56 | $ git reset --hard develop 57 | $ 58 | $ # Using a custom commit message for the merge below 59 | $ git merge -m 'Merge -s our (where _ours_ is develop) releasing stream x.y.z.' -s ours origin/main 60 | $ git push origin main 61 | ``` 62 | 63 | ## Pull Requests 64 | 65 | - Develop features and bug fixes in _topic_ branches. 66 | - _Topic_ branches can live in forks (external contributors) or within this repository (committers). 67 | \*\* When creating _topic_ branches in this repository please prefix with `/`. 68 | 69 | ### Merging Pull Requests 70 | 71 | - Pull request merging is restricted to squash & merge only. 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Salesforce Platform 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lwc-builder-ui 2 | 3 | LWC Builder is a tool to build, preview and configure Lightning Web Component (LWC) files to be used on the Salesforce Platform. 4 | Configure your component settings with clicks, then press "Create" button to create component files. 5 | 6 | ## Installation 7 | 8 | Now available on VSCode Marketplace 9 | https://marketplace.visualstudio.com/items?itemName=ninoish.lwc-builder 10 | 11 | ## How to start? 12 | 13 | Start simple by running `yarn watch` (or `npm run watch`, if you set up the project with `npm`). This will start the project with a local development server. 14 | 15 | The source files are located in the [`src`](./src) folder. All web components are within the [`src/modules`](./src/modules) folder. The folder hierarchy also represents the naming structure of the web components. 16 | 17 | This tool is built with [LWC](https://lwc.dev). 18 | 19 | # Contributing 20 | 21 | Anyone is welcome to contribute. 22 | Please follow [CONTRIBUTING.md](https://github.com/developerforce/lwc-builder/blob/main/CONTRIBUTING.md). 23 | 24 | # Disclaimer 25 | 26 | This tool is intended for experienced developers with LWC dev. 27 | It doesn't ensure code integrity. Please confirm the code before you deploy. 28 | 29 | # License 30 | 31 | The code is available under the [BSD 3-Clause license](https://github.com/developerforce/lwc-builder/blob/main/LICENSE). 32 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /lwc.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | { 4 | "dir": "src/modules" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lwc-builder-ui", 3 | "description": "This package is a UI part of LWC Builder that enables you to configure Lightning Web Component (LWC) Bundle on VSCode", 4 | "version": "0.1.31", 5 | "author": "@ninoish", 6 | "bugs": "https://github.com/developerforce/lwc-builder-ui/issues", 7 | "main": "dist/", 8 | "dependencies": { 9 | "change-case": "^4.1.2" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.15.0", 13 | "@lwc/eslint-plugin-lwc": "^1.0.1", 14 | "@lwc/jest-preset": "^11.0.1", 15 | "@lwc/rollup-plugin": "^2.3.1", 16 | "@rollup/plugin-node-resolve": "^13.0.4", 17 | "@rollup/plugin-replace": "^3.0.0", 18 | "@salesforce/eslint-config-lwc": "^2.1.1", 19 | "@salesforce/eslint-plugin-lightning": "^0.1.1", 20 | "babel-eslint": "^10.1.0", 21 | "eslint": "^7.32.0", 22 | "eslint-config-prettier": "^8.3.0", 23 | "eslint-plugin-header": "^3.1.1", 24 | "eslint-plugin-import": "^2.24.1", 25 | "eslint-plugin-inclusive-language": "^2.1.1", 26 | "eslint-plugin-jest": "^24.4.0", 27 | "eslint-plugin-jsdoc": "^36.0.7", 28 | "eslint-plugin-prettier": "^4.0.0", 29 | "fs-extra": "^10.0.0", 30 | "husky": "^7.0.1", 31 | "jest": "^26.6.3", 32 | "lwc": "^2.3.1", 33 | "prettier": "^2.3.2", 34 | "rollup": "^2.56.2", 35 | "rollup-plugin-copy-glob": "^0.3.2", 36 | "rollup-plugin-livereload": "^2.0.5", 37 | "rollup-plugin-serve": "^1.1.0", 38 | "rollup-plugin-terser": "^7.0.2" 39 | }, 40 | "engines": { 41 | "node": ">=10.13.0", 42 | "npm": ">=6.4.1" 43 | }, 44 | "homepage": "https://github.com/developerforce/lwc-builder-ui", 45 | "jest": { 46 | "preset": "@lwc/jest-preset", 47 | "moduleNameMapper": { 48 | "^(my|domain)/(.+)$": "/src/modules/$1/$2/$2" 49 | } 50 | }, 51 | "keywords": [ 52 | "lwc", 53 | "Lightning Web Component", 54 | "Salesforce", 55 | "sfdx", 56 | "LWC Builder", 57 | "VSCode", 58 | "Visual Studio Code" 59 | ], 60 | "license": "BSD-3-Clause", 61 | "repository": "https://github.com/developerforce/lwc-builder-ui", 62 | "scripts": { 63 | "build": "rollup --config ./scripts/rollup.config.js --environment NODE_ENV:production", 64 | "build:development": "rollup --config ./scripts/rollup.config.js ", 65 | "prepublishOnly": "npm run build", 66 | "lint:verify": "eslint src --ext js", 67 | "lint": "eslint src --ext js --fix", 68 | "prettier": "prettier --write \"**/*.{css,html,js,json,md,ts,yaml,yml}\"", 69 | "prettier:verify": "prettier --list-different \"**/*.{css,html,js,json,md,ts,yaml,yml}\"", 70 | "postinstall": "node -e \"if(require('fs').existsSync('.git')){process.exit(1)}\" || is-ci || husky install", 71 | "test:unit": "jest", 72 | "test:unit:coverage": "jest --coverage", 73 | "test:unit:debug": "jest --debug", 74 | "test:unit:watch": "jest --watch", 75 | "watch": "rollup --config ./scripts/rollup.config.js --watch" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "strict": "error" 4 | } 5 | } -------------------------------------------------------------------------------- /scripts/plugin-html.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | const fs = require('fs'); 9 | 10 | module.exports = () => ({ 11 | name: 'html', 12 | buildStart() { 13 | this.addWatchFile('src/index.html'); 14 | }, 15 | generateBundle() { 16 | let source = fs.readFileSync('src/index.html', 'utf-8'); 17 | 18 | this.emitFile({ 19 | type: 'asset', 20 | source, 21 | name: 'HTML Asset', 22 | fileName: 'index.html' 23 | }); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | const html = require('./plugin-html'); 9 | const lwc = require('@lwc/rollup-plugin'); 10 | const path = require('path'); 11 | const replace = require('@rollup/plugin-replace'); 12 | const { terser } = require('rollup-plugin-terser'); 13 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 14 | import copy from 'rollup-plugin-copy-glob'; 15 | import serve from 'rollup-plugin-serve'; 16 | import livereload from 'rollup-plugin-livereload'; 17 | 18 | const input = path.resolve(process.cwd(), 'src', 'index.js'); 19 | const outputDir = path.resolve(process.cwd(), 'dist'); 20 | const ASSETS = [{ files: 'src/resources/**', dest: 'dist/resources/' }]; 21 | 22 | module.exports = () => { 23 | const isProduction = process.env.NODE_ENV === 'production'; 24 | const isWatch = process.env.ROLLUP_WATCH; 25 | return { 26 | input, 27 | output: { 28 | file: path.join(outputDir, 'index.js'), 29 | format: 'esm' 30 | }, 31 | plugins: [ 32 | html(), 33 | nodeResolve(), 34 | lwc({ 35 | rootDir: 'src/modules' 36 | }), 37 | replace({ 38 | preventAssignment: true, 39 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 40 | }), 41 | copy(ASSETS, { watch: false }), 42 | isProduction && terser(), 43 | isWatch && serve('dist'), 44 | isWatch && livereload('dist') 45 | ], 46 | watch: { 47 | exclude: ['node_modules/**'] 48 | } 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LWC Builder | Online LWC Configuration Tool 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import '@lwc/synthetic-shadow'; 8 | import { createElement } from 'lwc'; 9 | import MyApp from 'my/app'; 10 | 11 | const app = createElement('my-app', { is: MyApp }); 12 | // eslint-disable-next-line @lwc/lwc/no-document-query 13 | document.querySelector('#main').appendChild(app); 14 | -------------------------------------------------------------------------------- /src/modules/domain/lwcBuilderEvent/lwcBuilderEvent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | class Payload { 9 | componentName; 10 | css; 11 | html; 12 | js; 13 | meta; 14 | svg; 15 | test; 16 | 17 | constructor(component) { 18 | this.componentName = component.componentName; 19 | this.js = component.js; 20 | this.meta = component.meta; 21 | if (component.withCss) this.css = component.css; 22 | if (component.withHtml) this.html = component.html; 23 | if (component.withSvg) this.svg = component.svg; 24 | if (component.withTest) this.test = component.test; 25 | } 26 | } 27 | 28 | export default class LWCBuilderEvent { 29 | type; // Options: create_button_clicked 30 | payload; // Payload object 31 | 32 | constructor(type, payload) { 33 | this.type = type; 34 | this.payload = new Payload(payload); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | }, 5 | "typeAcquisition": { 6 | "include": ["jest"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/my/app/__tests__/app.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { createElement } from 'lwc'; 9 | import MyApp from 'my/app'; 10 | import LWCBuilderEvent from 'domain/lwcBuilderEvent'; 11 | 12 | // Mock vscode API 13 | const mockPostMessage = jest.fn(); 14 | window.acquireVsCodeApi = jest.fn(() => { 15 | return { 16 | postMessage: mockPostMessage 17 | }; 18 | }); 19 | 20 | describe('my-app', () => { 21 | afterEach(() => { 22 | // The jsdom instance is shared across test cases in a single file so reset the DOM 23 | while (document.body.firstChild) { 24 | document.body.removeChild(document.body.firstChild); 25 | } 26 | }); 27 | 28 | it('show contents and enables button when component name indicated', () => { 29 | // GIVEN 30 | const element = createElement('my-app', { 31 | is: MyApp 32 | }); 33 | document.body.appendChild(element); 34 | 35 | // WHEN 36 | const form = element.shadowRoot.querySelector('my-form'); 37 | form.dispatchEvent( 38 | new CustomEvent('updatecontent', { 39 | detail: { componentName: 'MyNewCmp' } 40 | }) 41 | ); 42 | 43 | // THEN 44 | // Return a promise to wait for any asynchronous DOM updates. Jest 45 | // will automatically wait for the Promise chain to complete before 46 | // ending the test and fail the test if the promise rejects. 47 | return Promise.resolve().then(() => { 48 | const preview = element.shadowRoot.querySelector('my-preview'); 49 | const button = element.shadowRoot.querySelector('button:not([disabled])'); 50 | expect(preview).not.toBeNull(); 51 | expect(button).not.toBeNull(); 52 | }); 53 | }); 54 | 55 | it("doesn't show contents and disables button when component name not indicated", () => { 56 | // GIVEN 57 | const element = createElement('my-app', { 58 | is: MyApp 59 | }); 60 | document.body.appendChild(element); 61 | 62 | // WHEN 63 | const form = element.shadowRoot.querySelector('my-form'); 64 | form.dispatchEvent(new CustomEvent('updatecontent', { detail: {} })); 65 | 66 | // THEN 67 | // Return a promise to wait for any asynchronous DOM updates. Jest 68 | // will automatically wait for the Promise chain to complete before 69 | // ending the test and fail the test if the promise rejects. 70 | return Promise.resolve().then(() => { 71 | const preview = element.shadowRoot.querySelector('my-preview'); 72 | const button = element.shadowRoot.querySelector('button[disabled]'); 73 | expect(preview).toBeNull(); 74 | expect(button).not.toBeNull(); 75 | }); 76 | }); 77 | 78 | it('sends message to server when button clicked', () => { 79 | // GIVEN 80 | const element = createElement('my-app', { 81 | is: MyApp 82 | }); 83 | 84 | document.body.appendChild(element); 85 | const form = element.shadowRoot.querySelector('my-form'); 86 | form.dispatchEvent( 87 | new CustomEvent('updatecontent', { 88 | detail: { componentName: 'MyNewCmp' } 89 | }) 90 | ); 91 | 92 | // WHEN 93 | return Promise.resolve().then(() => { 94 | const button = element.shadowRoot.querySelector('button:not([disabled])'); 95 | button.click(); 96 | 97 | // THEN 98 | expect(mockPostMessage).toBeCalledWith( 99 | new LWCBuilderEvent('create_button_clicked', { 100 | componentName: 'MyNewCmp' 101 | }) 102 | ); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /src/modules/my/app/app.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | max-width: 1080px; 3 | width: 96%; 4 | margin: 1rem auto; 5 | color: #333; 6 | } 7 | .bottom-nav { 8 | z-index: 100; 9 | position: fixed; 10 | bottom: 0; 11 | right: 0; 12 | left: 0; 13 | padding: 0.75rem; 14 | background-color: rgba(255, 255, 255, 0.98); 15 | display: flex; 16 | flex-direction: row; 17 | justify-content: flex-end; 18 | align-items: center; 19 | border-top: 1px solid #cacaca; 20 | } 21 | .bottom-nav > * { 22 | margin: 0 0.5rem; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/my/app/app.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /src/modules/my/app/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, track } from 'lwc'; 8 | import LWCBuilderEvent from 'domain/lwcBuilderEvent'; 9 | 10 | export default class App extends LightningElement { 11 | @track contents; 12 | vscode; 13 | 14 | connectedCallback() { 15 | if (typeof acquireVsCodeApi === 'function') { 16 | this.vscode = acquireVsCodeApi(); // eslint-disable-line 17 | } 18 | } 19 | 20 | onUpdateForm(event) { 21 | this.contents = event.detail; 22 | } 23 | 24 | onButtonClick() { 25 | // Send message to server 26 | const message = new LWCBuilderEvent('create_button_clicked', this.contents); 27 | console.log(this.contents); 28 | this.vscode?.postMessage(message); 29 | } 30 | 31 | get hasContents() { 32 | return this.contents && this.contents.componentName; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/__tests__/buildContents.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { buildContents } from '../buildContents'; 9 | 10 | jest.mock('../buildCss', () => ({ buildCss: jest.fn(() => 'css') })); 11 | jest.mock('../buildHtml', () => ({ buildHtml: jest.fn(() => 'html') })); 12 | jest.mock('../buildJs', () => ({ buildJs: jest.fn(() => 'js') })); 13 | jest.mock('../buildMeta', () => ({ buildMeta: jest.fn(() => 'meta') })); 14 | jest.mock('../buildSvg', () => ({ buildSvg: jest.fn(() => 'svg') })); 15 | jest.mock('../buildTest', () => ({ buildTest: jest.fn(() => 'test') })); 16 | 17 | describe('my-build-contents', () => { 18 | afterEach(() => { 19 | // The jsdom instance is shared across test cases in a single file so reset the DOM 20 | while (document.body.firstChild) { 21 | document.body.removeChild(document.body.firstChild); 22 | } 23 | }); 24 | 25 | it('build calls all build functions', () => { 26 | // GIVEN 27 | const contents = {}; 28 | 29 | // WHEN 30 | const result = buildContents(contents); 31 | 32 | // THEN 33 | expect(result).toMatchObject({ 34 | ...contents, 35 | html: 'html', 36 | js: 'js', 37 | css: 'css', 38 | meta: 'meta', 39 | svg: 'svg', 40 | test: 'test' 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/__tests__/buildCss.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { buildCss } from '../buildCss'; 9 | 10 | describe('my-build-css', () => { 11 | afterEach(() => { 12 | // The jsdom instance is shared across test cases in a single file so reset the DOM 13 | while (document.body.firstChild) { 14 | document.body.removeChild(document.body.firstChild); 15 | } 16 | }); 17 | 18 | it('css returns default css', () => { 19 | // GIVEN 20 | const contents = {}; 21 | 22 | // WHEN 23 | const css = buildCss(contents); 24 | 25 | // THEN 26 | expect(css).toBe(`h1 {}`); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/__tests__/buildHtml.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { buildHtml } from '../buildHtml'; 9 | 10 | describe('my-build-html', () => { 11 | afterEach(() => { 12 | // The jsdom instance is shared across test cases in a single file so reset the DOM 13 | while (document.body.firstChild) { 14 | document.body.removeChild(document.body.firstChild); 15 | } 16 | }); 17 | 18 | it('html includes component name', () => { 19 | // GIVEN 20 | const contents = { componentName: 'MyLWC' }; 21 | 22 | // WHEN 23 | const html = buildHtml(contents); 24 | 25 | // THEN 26 | expect(html).toBe(``); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/__tests__/buildJs.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { buildJs } from '../buildJs'; 9 | import { pascalCase } from 'change-case'; 10 | 11 | const buildTargets = (option) => { 12 | const targetsArr = [ 13 | { name: 'AppPage', value: 'lightning__AppPage' }, 14 | { name: 'HomePage', value: 'lightning__HomePage' }, 15 | { name: 'RecordPage', value: 'lightning__RecordPage' }, 16 | { name: 'RecordAction', value: 'lightning__RecordAction' }, 17 | { name: 'UtilityBar', value: 'lightning__UtilityBar' }, 18 | { name: 'FlowScreen', value: 'lightning__FlowScreen' }, 19 | { name: 'Tab', value: 'lightning__Tab' }, 20 | { name: 'Inbox', value: 'lightning__Inbox' }, 21 | { name: 'CommunityPage', value: 'lightningCommunity__Page' }, 22 | { name: 'CommunityDefault', value: 'lightningCommunity__Default' }, 23 | { name: 'CommunityPageLayout', value: 'lightningCommunity__Page_Layout' }, 24 | { name: 'CommunityThemeLayout', value: 'lightningCommunity__Theme_Layout' }, 25 | { name: 'SnapinChatMessage', value: 'lightningSnapin__ChatMessage' }, 26 | { name: 'SnapinMinimized', value: 'lightningSnapin__Minimized' }, 27 | { name: 'SnapinPreChat', value: 'lightningSnapin__PreChat' }, 28 | { name: 'SnapinChatHeader', value: 'lightningSnapin__ChatHeader' }, 29 | { 30 | name: 'Account Engagement (Pardot) Email', 31 | value: 'lightningStatic__Email' 32 | }, 33 | { name: 'CRM Analytics dashboard', value: 'analytics__Dashboard' }, 34 | { name: 'VoiceExtension', value: 'lightning__VoiceExtension' }, 35 | { name: 'EnablementProgram', value: 'lightning__EnablementProgram' }, 36 | { name: 'UrlAddressable', value: 'lightning__UrlAddressable' } 37 | ]; 38 | const targets = {}; 39 | targetsArr.forEach((t) => { 40 | targets[t.value] = { 41 | name: t.name, 42 | value: t.value, 43 | enabled: false, 44 | small: false, 45 | large: false, 46 | headlessAction: false, 47 | hasStep: false, 48 | properties: [], 49 | objects: [] 50 | }; 51 | if (option) { 52 | for (const v in option) { 53 | if (Object.prototype.hasOwnProperty.call(option, v)) { 54 | targets[v] = { ...targets[v], ...option[v] }; 55 | } 56 | } 57 | } 58 | }); 59 | return targets; 60 | }; 61 | 62 | describe('my-build-js', () => { 63 | afterEach(() => { 64 | // The jsdom instance is shared across test cases in a single file so reset the DOM 65 | while (document.body.firstChild) { 66 | document.body.removeChild(document.body.firstChild); 67 | } 68 | }); 69 | 70 | it('returns correct js when there are api props', () => { 71 | // GIVEN 72 | const contents = { 73 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 74 | targets: buildTargets({ 75 | lightning__Inbox: { enabled: false }, 76 | lightningSnapin__ChatMessage: { enabled: false }, 77 | lightningSnapin__Minimized: { enabled: false }, 78 | lightningSnapin__PreChat: { enabled: false }, 79 | lightningSnapin__ChatHeader: { enabled: false } 80 | }), 81 | componentName: 'MyLWC' 82 | }; 83 | 84 | // WHEN 85 | const js = buildJs(contents); 86 | 87 | // THEN 88 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 89 | expectedJs += `export default class ${pascalCase( 90 | contents.componentName 91 | )} extends LightningElement {\n`; 92 | expectedJs += `\t@api myProp1;\n`; 93 | expectedJs += `\t@api myProp2;\n`; 94 | expectedJs += `}`; 95 | 96 | expect(js).toBe(expectedJs); 97 | }); 98 | 99 | it('returns correct js when lightning__Inbox is enabled', () => { 100 | // GIVEN 101 | const contents = { 102 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 103 | targets: buildTargets({ 104 | lightning__Inbox: { enabled: true }, 105 | lightningSnapin__ChatMessage: { enabled: false }, 106 | lightningSnapin__Minimized: { enabled: false }, 107 | lightningSnapin__PreChat: { enabled: false }, 108 | lightningSnapin__ChatHeader: { enabled: false } 109 | }), 110 | componentName: 'MyLWC' 111 | }; 112 | 113 | // WHEN 114 | const js = buildJs(contents); 115 | 116 | // THEN 117 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 118 | expectedJs += `export default class ${pascalCase( 119 | contents.componentName 120 | )} extends LightningElement {\n`; 121 | expectedJs += `\t@api myProp1;\n`; 122 | expectedJs += `\t@api myProp2;\n`; 123 | expectedJs += `\t@api dates;\n`; 124 | expectedJs += `\t@api emails;\n`; 125 | expectedJs += `\t@api location;\n`; 126 | expectedJs += `\t@api messageBody;\n`; 127 | expectedJs += `\t@api mode;\n`; 128 | expectedJs += `\t@api people;\n`; 129 | expectedJs += `\t@api source;\n`; 130 | expectedJs += `\t@api subject;\n`; 131 | expectedJs += `}`; 132 | 133 | expect(js).toBe(expectedJs); 134 | }); 135 | 136 | it('returns correct js when lightningSnapin__ChatMessage enabled', () => { 137 | // GIVEN 138 | const contents = { 139 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 140 | targets: buildTargets({ 141 | lightning__Inbox: { enabled: false }, 142 | lightningSnapin__ChatMessage: { enabled: true }, 143 | lightningSnapin__Minimized: { enabled: false }, 144 | lightningSnapin__PreChat: { enabled: false }, 145 | lightningSnapin__ChatHeader: { enabled: false } 146 | }), 147 | componentName: 'MyLWC' 148 | }; 149 | 150 | // WHEN 151 | const js = buildJs(contents); 152 | 153 | // THEN 154 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 155 | expectedJs += `import BaseChatMessage from 'lightningsnapin/baseChatMessage';\n`; 156 | expectedJs += `export default class ${pascalCase( 157 | contents.componentName 158 | )} extends BaseChatMessage {\n`; 159 | expectedJs += `\t@api myProp1;\n`; 160 | expectedJs += `\t@api myProp2;\n`; 161 | expectedJs += `}`; 162 | 163 | expect(js).toBe(expectedJs); 164 | }); 165 | 166 | it('returns correct js when lightningSnapin__Minimized enabled', () => { 167 | // GIVEN 168 | const contents = { 169 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 170 | targets: buildTargets({ 171 | lightning__Inbox: { enabled: false }, 172 | lightningSnapin__ChatMessage: { enabled: false }, 173 | lightningSnapin__Minimized: { enabled: true }, 174 | lightningSnapin__PreChat: { enabled: false }, 175 | lightningSnapin__ChatHeader: { enabled: false } 176 | }), 177 | componentName: 'MyLWC' 178 | }; 179 | 180 | // WHEN 181 | const js = buildJs(contents); 182 | 183 | // THEN 184 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 185 | expectedJs += `import { assignHandler, maximize } from 'lightningsnapin/minimized';\n`; 186 | expectedJs += `export default class ${pascalCase( 187 | contents.componentName 188 | )} extends LightningElement {\n`; 189 | expectedJs += `\t@api myProp1;\n`; 190 | expectedJs += `\t@api myProp2;\n`; 191 | expectedJs += `}`; 192 | 193 | expect(js).toBe(expectedJs); 194 | }); 195 | 196 | it('returns correct js when lightningSnapin__PreChat enabled', () => { 197 | // GIVEN 198 | const contents = { 199 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 200 | targets: buildTargets({ 201 | lightning__Inbox: { enabled: false }, 202 | lightningSnapin__ChatMessage: { enabled: false }, 203 | lightningSnapin__Minimized: { enabled: false }, 204 | lightningSnapin__PreChat: { enabled: true }, 205 | lightningSnapin__ChatHeader: { enabled: false } 206 | }), 207 | componentName: 'MyLWC' 208 | }; 209 | 210 | // WHEN 211 | const js = buildJs(contents); 212 | 213 | // THEN 214 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 215 | expectedJs += `import BasePrechat from 'lightningsnapin/basePrechat';\n`; 216 | expectedJs += `export default class ${pascalCase( 217 | contents.componentName 218 | )} extends BasePrechat {\n`; 219 | expectedJs += `\t@api myProp1;\n`; 220 | expectedJs += `\t@api myProp2;\n`; 221 | expectedJs += `}`; 222 | expect(js).toBe(expectedJs); 223 | }); 224 | 225 | it('returns correct js when lightningSnapin__ChatHeader enabled', () => { 226 | // GIVEN 227 | const contents = { 228 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 229 | targets: buildTargets({ 230 | lightning__Inbox: { enabled: false }, 231 | lightningSnapin__ChatMessage: { enabled: false }, 232 | lightningSnapin__Minimized: { enabled: false }, 233 | lightningSnapin__PreChat: { enabled: false }, 234 | lightningSnapin__ChatHeader: { enabled: true } 235 | }), 236 | componentName: 'MyLWC' 237 | }; 238 | 239 | // WHEN 240 | const js = buildJs(contents); 241 | 242 | // THEN 243 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 244 | expectedJs += `import BaseChatHeader from 'lightningsnapin/baseChatHeader';\n`; 245 | expectedJs += `export default class ${pascalCase( 246 | contents.componentName 247 | )} extends BaseChatHeader {\n`; 248 | expectedJs += `\t@api myProp1;\n`; 249 | expectedJs += `\t@api myProp2;\n`; 250 | expectedJs += `}`; 251 | 252 | expect(js).toBe(expectedJs); 253 | }); 254 | 255 | it('returns correct js when lightningStatic__Email enabled', () => { 256 | // GIVEN 257 | const contents = { 258 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 259 | targets: buildTargets({ 260 | lightningStatic__Email: { enabled: true } 261 | }), 262 | componentName: 'MyLWC' 263 | }; 264 | 265 | // WHEN 266 | const js = buildJs(contents); 267 | 268 | // THEN 269 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 270 | expectedJs += `export default class ${pascalCase( 271 | contents.componentName 272 | )} extends LightningElement {\n`; 273 | expectedJs += `\t@api myProp1;\n`; 274 | expectedJs += `\t@api myProp2;\n`; 275 | expectedJs += `}`; 276 | 277 | expect(js).toBe(expectedJs); 278 | }); 279 | 280 | it('returns correct js when lightningStatic__Email enabled and having Alignment properties', () => { 281 | // GIVEN 282 | const contents = { 283 | properties: [ 284 | { name: 'myProp1' }, 285 | { name: 'myProp2' }, 286 | { name: 'alignment', type: 'HorizontalAlignment', default: 'center' } 287 | ], 288 | targets: buildTargets({ 289 | lightningStatic__Email: { enabled: true } 290 | }), 291 | componentName: 'MyLWC' 292 | }; 293 | 294 | // WHEN 295 | const js = buildJs(contents); 296 | 297 | // THEN 298 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 299 | expectedJs += `export default class ${pascalCase( 300 | contents.componentName 301 | )} extends LightningElement {\n`; 302 | expectedJs += `\t@api myProp1;\n`; 303 | expectedJs += `\t@api myProp2;\n`; 304 | expectedJs += `\t@api alignment;\n`; 305 | expectedJs += `}`; 306 | 307 | expect(js).toBe(expectedJs); 308 | }); 309 | 310 | it('returns correct js when analytics__Dashboard enabled', () => { 311 | // GIVEN 312 | const contents = { 313 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 314 | targets: buildTargets({ 315 | analytics__Dashboard: { enabled: true, hasStep: true } 316 | }), 317 | componentName: 'MyLWC' 318 | }; 319 | 320 | // WHEN 321 | const js = buildJs(contents); 322 | 323 | // THEN 324 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 325 | expectedJs += `export default class ${pascalCase( 326 | contents.componentName 327 | )} extends LightningElement {\n`; 328 | expectedJs += `\t@api myProp1;\n`; 329 | expectedJs += `\t@api myProp2;\n`; 330 | expectedJs += `\t@api getState;\n`; 331 | expectedJs += `\t@api setState;\n`; 332 | expectedJs += `\t@api refresh;\n`; 333 | expectedJs += `\t@api results;\n`; 334 | expectedJs += `\t@api metadata;\n`; 335 | expectedJs += `\t@api selectMode;\n`; 336 | expectedJs += `\t@api selection;\n`; 337 | expectedJs += `\t@api setSelection;\n`; 338 | expectedJs += `\t@api\n\tstateChangedCallback(prevState, newState) {\n\t}\n`; 339 | expectedJs += `}`; 340 | 341 | expect(js).toBe(expectedJs); 342 | }); 343 | 344 | it('returns correct js when lightning__VoiceExtension enabled', () => { 345 | // GIVEN 346 | const contents = { 347 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 348 | targets: buildTargets({ 349 | lightning__VoiceExtension: { enabled: true } 350 | }), 351 | componentName: 'MyLWC' 352 | }; 353 | 354 | // WHEN 355 | const js = buildJs(contents); 356 | 357 | // THEN 358 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 359 | expectedJs += `export default class ${pascalCase( 360 | contents.componentName 361 | )} extends LightningElement {\n`; 362 | expectedJs += `\t@api myProp1;\n`; 363 | expectedJs += `\t@api myProp2;\n`; 364 | expectedJs += `}`; 365 | 366 | expect(js).toBe(expectedJs); 367 | }); 368 | 369 | it('returns correct js when lightning__EnablementProgram enabled', () => { 370 | // GIVEN 371 | const contents = { 372 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 373 | targets: buildTargets({ 374 | lightning__EnablementProgram: { enabled: true } 375 | }), 376 | componentName: 'MyLWC' 377 | }; 378 | 379 | // WHEN 380 | const js = buildJs(contents); 381 | 382 | // THEN 383 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 384 | expectedJs += `export default class ${pascalCase( 385 | contents.componentName 386 | )} extends LightningElement {\n`; 387 | expectedJs += `\t@api myProp1;\n`; 388 | expectedJs += `\t@api myProp2;\n`; 389 | expectedJs += `}`; 390 | 391 | expect(js).toBe(expectedJs); 392 | }); 393 | 394 | it('returns correct js when lightning__UrlAddressable enabled', () => { 395 | // GIVEN 396 | const contents = { 397 | properties: [{ name: 'myProp1' }, { name: 'myProp2' }], 398 | targets: buildTargets({ 399 | lightning__UrlAddressable: { enabled: true } 400 | }), 401 | componentName: 'MyLWC' 402 | }; 403 | 404 | // WHEN 405 | const js = buildJs(contents); 406 | 407 | // THEN 408 | let expectedJs = `import { LightningElement, api } from "lwc";\n`; 409 | expectedJs += `export default class ${pascalCase( 410 | contents.componentName 411 | )} extends LightningElement {\n`; 412 | expectedJs += `\t@api myProp1;\n`; 413 | expectedJs += `\t@api myProp2;\n`; 414 | expectedJs += `}`; 415 | 416 | expect(js).toBe(expectedJs); 417 | }); 418 | }); 419 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/__tests__/buildSvg.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { buildSvg } from '../buildSvg'; 9 | 10 | describe('my-build-svg', () => { 11 | afterEach(() => { 12 | // The jsdom instance is shared across test cases in a single file so reset the DOM 13 | while (document.body.firstChild) { 14 | document.body.removeChild(document.body.firstChild); 15 | } 16 | }); 17 | 18 | it('svg returns passed in svg when indicated', () => { 19 | // GIVEN 20 | const contents = { svgFileContent: 'IAmAnSvg' }; 21 | 22 | // WHEN 23 | const svg = buildSvg(contents); 24 | 25 | // THEN 26 | expect(svg).toBe(contents.svgFileContent); 27 | }); 28 | 29 | it('svg returns default svg when svg not passed in', () => { 30 | // GIVEN 31 | const contents = {}; 32 | 33 | // WHEN 34 | const svg = buildSvg(contents); 35 | 36 | // THEN 37 | expect(svg).toBe(` 38 | 39 | 40 | 41 | `); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/__tests__/buildTest.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { buildTest } from '../buildTest'; 9 | import { pascalCase, camelCase, paramCase } from 'change-case'; 10 | 11 | describe('my-build-contents', () => { 12 | afterEach(() => { 13 | // The jsdom instance is shared across test cases in a single file so reset the DOM 14 | while (document.body.firstChild) { 15 | document.body.removeChild(document.body.firstChild); 16 | } 17 | }); 18 | 19 | it('test includes passed in componentName correctly formatted', () => { 20 | // GIVEN 21 | const contents = { componentName: 'MyLWC' }; 22 | 23 | // WHEN 24 | const test = buildTest(contents); 25 | 26 | // THEN 27 | const pascal = pascalCase(contents.componentName); 28 | const param = paramCase(contents.componentName); 29 | const camel = camelCase(contents.componentName); 30 | expect(test).toBe(`import { createElement } from "lwc"; 31 | import ${pascal} from "c/${camel}"; 32 | 33 | // import { registerLdsTestWireAdapter } from '@salesforce/sfdx-lwc-jest'; 34 | // import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest'; 35 | 36 | describe("c-${param}", () => { 37 | afterEach(() => { 38 | while (document.body.firstChild) { 39 | document.body.removeChild(document.body.firstChild); 40 | } 41 | jest.clearAllMocks(); 42 | }); 43 | 44 | it("has component name on the header", () => { 45 | const element = createElement("c-${param}", { 46 | is: ${pascal} 47 | }); 48 | document.body.appendChild(element); 49 | 50 | return Promise.resolve().then(() => { 51 | const componentHeader = element.shadowRoot.querySelector("h1"); 52 | expect(componentHeader.textContent).toBe("${contents.componentName}"); 53 | }); 54 | }); 55 | });`); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/buildContents.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { buildHtml } from './buildHtml'; 8 | import { buildJs } from './buildJs'; 9 | import { buildMeta } from './buildMeta'; 10 | import { buildCss } from './buildCss'; 11 | import { buildSvg } from './buildSvg'; 12 | import { buildTest } from './buildTest'; 13 | 14 | export const buildContents = (contents) => { 15 | const html = buildHtml(contents); 16 | const js = buildJs(contents); 17 | const css = buildCss(contents); 18 | const svg = buildSvg(contents); 19 | const meta = buildMeta(contents); 20 | const test = buildTest(contents); 21 | return { ...contents, html, js, css, meta, svg, test }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/buildCss.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | export const buildCss = () => { 8 | // TODO: apply responsive properties if set 9 | // https://developer.salesforce.com/docs/atlas.en-us.exp_cloud_lwr.meta/exp_cloud_lwr/get_started_responsive_properties.htm 10 | return `h1 {}`; 11 | }; 12 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/buildHtml.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | export const buildHtml = (contents) => { 8 | const { targets, componentName } = contents; 9 | if ( 10 | targets?.lightning__RecordAction?.enabled && 11 | !targets?.lightning__RecordAction?.headlessAction 12 | ) { 13 | return ``; 22 | } 23 | 24 | if ( 25 | targets?.lightning__RecordAction?.enabled && 26 | targets?.lightning__RecordAction?.headlessAction 27 | ) { 28 | return ``; 29 | } 30 | 31 | return ``; 34 | }; 35 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/buildJs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { pascalCase } from 'change-case'; 8 | 9 | export const buildJs = (contents) => { 10 | const { properties, targets, componentName } = contents; 11 | 12 | const propNames = properties.map((p) => p.name); 13 | 14 | // https://developer.salesforce.com/docs/component-library/documentation/en/lwc/use_config_for_app_builder_email_app_pane 15 | const inboxProps = [ 16 | 'dates', 17 | 'emails', 18 | 'location', 19 | 'messageBody', 20 | 'mode', 21 | 'people', 22 | 'source', 23 | 'subject' 24 | ]; 25 | 26 | const recordRelatedProps = ['recordId', 'objectApiName']; 27 | 28 | const analyticsWithStepProps = [ 29 | 'results', 30 | 'metadata', 31 | 'selectMode', 32 | 'selection', 33 | 'setSelection' 34 | ]; 35 | const analyticsProps = ['getState', 'setState', 'refresh']; 36 | 37 | const apis = [ 38 | ...new Set([ 39 | ...propNames, 40 | ...(targets.lightning__Inbox.enabled ? inboxProps : []), 41 | ...(targets.lightning__RecordPage.enabled || 42 | targets.lightning__RecordAction.enabled 43 | ? recordRelatedProps 44 | : []), 45 | ...(targets.analytics__Dashboard.enabled ? analyticsProps : []), 46 | ...(targets.analytics__Dashboard.enabled && 47 | targets.analytics__Dashboard.hasStep 48 | ? analyticsWithStepProps 49 | : []) 50 | ]) 51 | ]; 52 | 53 | const hasProperties = apis && apis.length > 0; 54 | const pascal = pascalCase(componentName); 55 | let js = ''; 56 | js += `import { LightningElement${ 57 | hasProperties ? ', api' : '' 58 | } } from "lwc";\n`; 59 | 60 | let classInheritance = 'LightningElement'; 61 | if (targets.lightningSnapin__ChatMessage.enabled) { 62 | // https://developer.salesforce.com/docs/component-library/bundle/lightningsnapin-base-chat-message/documentation 63 | js += `import BaseChatMessage from 'lightningsnapin/baseChatMessage';\n`; 64 | classInheritance = 'BaseChatMessage'; 65 | } 66 | if (targets.lightningSnapin__Minimized.enabled) { 67 | // https://developer.salesforce.com/docs/component-library/bundle/lightningsnapin-minimized/documentation 68 | js += `import { assignHandler, maximize } from 'lightningsnapin/minimized';\n`; 69 | } 70 | if (targets.lightningSnapin__PreChat.enabled) { 71 | // https://developer.salesforce.com/docs/component-library/bundle/lightningsnapin-base-prechat/documentation 72 | js += `import BasePrechat from 'lightningsnapin/basePrechat';\n`; 73 | classInheritance = 'BasePrechat'; 74 | } 75 | if (targets.lightningSnapin__ChatHeader.enabled) { 76 | // https://developer.salesforce.com/docs/component-library/bundle/lightningsnapin-base-chat-header/documentation 77 | js += `import BaseChatHeader from 'lightningsnapin/baseChatHeader';\n`; 78 | classInheritance = 'BaseChatHeader'; 79 | } 80 | 81 | if ( 82 | targets.lightning__RecordAction.enabled && 83 | !targets.lightning__RecordAction.headlessAction 84 | ) { 85 | js += `import { CloseActionScreenEvent } from 'lightning/actions';\n`; 86 | } 87 | 88 | js += `export default class ${pascal} extends ${classInheritance} {\n`; 89 | js += apis 90 | .map((p) => { 91 | return p ? `\t@api ${p};\n` : null; 92 | }) 93 | .join(''); 94 | 95 | if (targets.lightning__RecordAction.enabled) { 96 | if (targets.lightning__RecordAction.headlessAction) { 97 | js += `\t@api\n\tinvoke() {\n\t\tconsole.log('headless quick action called');\n\t}\n`; 98 | } else { 99 | js += `\tcloseModal() {\n\t\tthis.dispatchEvent(new CloseActionScreenEvent());\n\t}\n`; 100 | } 101 | } 102 | 103 | // analytics callback 104 | if (targets.analytics__Dashboard.enabled) { 105 | js += `\t@api\n\tstateChangedCallback(prevState, newState) {\n\t}\n`; 106 | } 107 | 108 | js += `}`; 109 | 110 | return js; 111 | }; 112 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/buildMeta.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | export const buildMeta = (contents) => { 8 | const { 9 | apiVersion, 10 | isExposed, 11 | masterLabel, 12 | description, 13 | targets, 14 | properties, 15 | objects, 16 | configurationEditor 17 | } = contents; 18 | 19 | const enabledTargetArray = Object.values(targets).filter((t) => t.enabled); 20 | enabledTargetArray.forEach((t) => { 21 | t.properties = []; 22 | t.objects = []; 23 | }); 24 | 25 | properties.forEach((p) => { 26 | p.selectedTargets.forEach((st) => { 27 | enabledTargetArray.find((t) => t.value === st).properties.push(p); 28 | }); 29 | }); 30 | if (objects.length > 0) { 31 | const recordPage = enabledTargetArray.find( 32 | (t) => t.value === 'lightning__RecordPage' 33 | ); 34 | if (recordPage) { 35 | recordPage.objects = objects; 36 | } 37 | } 38 | 39 | let meta = ``; 40 | meta += `\n`; 41 | meta += `\t${apiVersion}\n`; 42 | meta += `\t${isExposed}\n`; 43 | if (masterLabel) { 44 | meta += `\t${masterLabel}\n`; 45 | } 46 | if (description) { 47 | meta += `\t${description}\n`; 48 | } 49 | if (enabledTargetArray.length > 0) { 50 | meta += `\t\n`; 51 | meta += enabledTargetArray 52 | .map((t) => `\t\t${t.value}`) 53 | .join('\n'); 54 | meta += `\n\t\n`; 55 | 56 | let targetConfigs = ''; 57 | if ( 58 | properties.length > 0 || 59 | objects.length > 0 || 60 | enabledTargetArray.find((t) => t.small || t.large || t.hasStep) || 61 | enabledTargetArray.find((t) => t.value === 'lightning__RecordAction') || 62 | (enabledTargetArray.length === 1 && 63 | enabledTargetArray[0].value === 'lightning__FlowScreen' && 64 | configurationEditor) 65 | ) { 66 | for (let t of enabledTargetArray) { 67 | // No targetConfig, property support 68 | if ( 69 | t.value === 'lightningCommunity__Page' || 70 | t.value === 'lightningCommunity__Page_Layout' || 71 | t.value === 'lightningCommunity__Theme_Layout' 72 | ) { 73 | continue; 74 | } 75 | if ( 76 | t.properties.length === 0 && 77 | t.objects.length === 0 && 78 | !( 79 | (t.value === 'lightning__AppPage' || 80 | t.value === 'lightning__HomePage' || 81 | t.value === 'lightning__RecordPage') && 82 | (t.small || t.large) 83 | ) && 84 | t.value !== 'lightning__RecordAction' && 85 | !( 86 | enabledTargetArray.length === 1 && 87 | enabledTargetArray[0].value === 'lightning__FlowScreen' && 88 | configurationEditor 89 | ) && 90 | !(t.value === 'analytics__Dashboard' && t.hasStep === true) 91 | ) { 92 | continue; 93 | } 94 | 95 | // custom property editor for Flow 96 | if ( 97 | enabledTargetArray.length === 1 && 98 | t.value === 'lightning__FlowScreen' && 99 | configurationEditor 100 | ) { 101 | targetConfigs += `\t\t\n`; 102 | } else { 103 | targetConfigs += `\t\t\n`; 104 | } 105 | 106 | targetConfigs += t.properties 107 | .map((p) => { 108 | let propAttributes = ` name="${p.name}"`; 109 | 110 | if (p.type === 'apex') { 111 | propAttributes += ` type="${p.apexClassName}"`; 112 | } else if (p.type === 'sobject') { 113 | propAttributes += ` type="${p.sObjectName}"`; 114 | } else { 115 | propAttributes += ` type="${p.type}"`; 116 | } 117 | 118 | if ( 119 | p.type === 'String' && 120 | p.datasource && 121 | t.value !== 'lightning__FlowScreen' 122 | ) { 123 | propAttributes += ` datasource="${p.datasource}"`; 124 | } 125 | if ( 126 | p.default && 127 | !( 128 | t.value === 'lightning__FlowScreen' && 129 | p.flowInput ^ p.flowOutput && 130 | p.flowOutput 131 | ) // No support for Flow outputOnly 132 | ) { 133 | propAttributes += ` default="${p.default}"`; 134 | } 135 | if (p.description) { 136 | propAttributes += ` description="${p.description}"`; 137 | } 138 | if ( 139 | p.type === 'Integer' && 140 | (p.min || p.min === 0) && 141 | t.value !== 'lightning__FlowScreen' 142 | ) { 143 | propAttributes += ` min="${p.min}"`; 144 | } 145 | if ( 146 | p.type === 'Integer' && 147 | (p.max || p.max === 0) && 148 | t.value !== 'lightning__FlowScreen' 149 | ) { 150 | propAttributes += ` max="${p.max}"`; 151 | } 152 | if (p.label) { 153 | propAttributes += ` label="${p.label}"`; 154 | } 155 | if ( 156 | p.type === 'String' && 157 | p.placeholder && 158 | t.value !== 'lightning__FlowScreen' 159 | ) { 160 | propAttributes += ` placeholder="${p.placeholder}"`; 161 | } 162 | if ( 163 | p.required && 164 | !( 165 | t.value === 'lightning__FlowScreen' && 166 | p.flowInput ^ p.flowOutput && 167 | p.flowOutput 168 | ) // No support for Flow outputOnly 169 | ) { 170 | propAttributes += ` required="true"`; 171 | } 172 | if ( 173 | t.value === 'lightning__FlowScreen' && 174 | p.flowInput ^ p.flowOutput 175 | ) { 176 | propAttributes += ` role="${ 177 | p.flowInput ? 'inputOnly' : 'outputOnly' 178 | }"`; 179 | } 180 | 181 | if ( 182 | p.type === 'ContentReference' && 183 | t.value === 'lightningCommunity__Default' && 184 | p.cmsFilters.length > 0 && 185 | p.cmsFilters.find((f) => !!f.value) 186 | ) { 187 | propAttributes += ` filter="${p.cmsFilters 188 | .filter((f) => !!f.value) 189 | .map((f) => f.value) 190 | .join(',')}"`; 191 | } 192 | 193 | // LWR Screen Responsive 194 | // https://developer.salesforce.com/docs/atlas.en-us.exp_cloud_lwr.meta/exp_cloud_lwr/get_started_responsive_properties.htm 195 | if ( 196 | p.type === 'Integer' && 197 | t.value === 'lightningCommunity__Default' && 198 | p.screenResponsive 199 | ) { 200 | propAttributes += ` screenResponsive="true" exposedTo="css"`; 201 | } 202 | 203 | return `\t\t\t`; 204 | }) 205 | .join('\n'); 206 | if (t.properties.length > 0) { 207 | targetConfigs += '\n'; 208 | } 209 | 210 | // Specific sObject targeting 211 | if (t.value === 'lightning__RecordPage' && t.objects.length > 0) { 212 | targetConfigs += `\t\t\t\n`; 213 | targetConfigs += t.objects 214 | .map((o) => { 215 | return `\t\t\t\t${o.name}`; 216 | }) 217 | .join('\n'); 218 | targetConfigs += `\n\t\t\t\n`; 219 | } 220 | 221 | // Action Type 222 | if (t.value === 'lightning__RecordAction') { 223 | targetConfigs += `\t\t\t${ 224 | t.headlessAction ? 'Action' : 'ScreenAction' 225 | }\n`; 226 | } 227 | 228 | // Form Factor 229 | if ( 230 | (t.value === 'lightning__AppPage' || 231 | t.value === 'lightning__HomePage' || 232 | t.value === 'lightning__RecordPage') && 233 | (t.small || t.large) 234 | ) { 235 | targetConfigs += `\t\t\t\n`; 236 | if (t.small) { 237 | targetConfigs += `\t\t\t\t\n`; 238 | } 239 | if (t.large) { 240 | targetConfigs += `\t\t\t\t\n`; 241 | } 242 | targetConfigs += `\t\t\t\n`; 243 | } 244 | 245 | // CRM Analytics 246 | if (t.value === 'analytics__Dashboard' && t.hasStep) { 247 | targetConfigs += `\t\t\ttrue\n`; 248 | } 249 | 250 | targetConfigs += `\t\t\n`; 251 | } 252 | } 253 | if (targetConfigs) { 254 | meta += `\t\n${targetConfigs}\t\n`; 255 | } 256 | } 257 | meta += ``; 258 | return meta; 259 | }; 260 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/buildSvg.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | export const buildSvg = (contents) => { 8 | if (contents.svgFileContent) { 9 | return contents.svgFileContent; 10 | } 11 | return ` 12 | 13 | 14 | 15 | `; 16 | }; 17 | -------------------------------------------------------------------------------- /src/modules/my/buildContents/buildTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { pascalCase, camelCase, paramCase } from 'change-case'; 8 | export const buildTest = (contents) => { 9 | const { componentName } = contents; 10 | const pascal = pascalCase(componentName); 11 | const param = paramCase(componentName); 12 | const camel = camelCase(componentName); 13 | return `import { createElement } from "lwc"; 14 | import ${pascal} from "c/${camel}"; 15 | 16 | // import { registerLdsTestWireAdapter } from '@salesforce/sfdx-lwc-jest'; 17 | // import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest'; 18 | 19 | describe("c-${param}", () => { 20 | afterEach(() => { 21 | while (document.body.firstChild) { 22 | document.body.removeChild(document.body.firstChild); 23 | } 24 | jest.clearAllMocks(); 25 | }); 26 | 27 | it("has component name on the header", () => { 28 | const element = createElement("c-${param}", { 29 | is: ${pascal} 30 | }); 31 | document.body.appendChild(element); 32 | 33 | return Promise.resolve().then(() => { 34 | const componentHeader = element.shadowRoot.querySelector("h1"); 35 | expect(componentHeader.textContent).toBe("${componentName}"); 36 | }); 37 | }); 38 | });`; 39 | }; 40 | -------------------------------------------------------------------------------- /src/modules/my/footer/footer.css: -------------------------------------------------------------------------------- 1 | footer { 2 | margin: 1.5rem auto 5.5rem; 3 | display: flex; 4 | flex-wrap: wrap; 5 | justify-content: space-between; 6 | padding: 0 0.25rem; 7 | } 8 | .footer-list { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-end; 12 | } 13 | .footer-list > li { 14 | display: inline-flex; 15 | align-items: center; 16 | } 17 | .footer-list > li > a { 18 | display: inline-flex; 19 | align-items: center; 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/my/footer/footer.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/modules/my/footer/footer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement } from 'lwc'; 8 | 9 | export default class Footer extends LightningElement {} 10 | -------------------------------------------------------------------------------- /src/modules/my/form/__tests__/form.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { createElement } from 'lwc'; 9 | import MyForm from 'my/form'; 10 | import { buildContents } from 'my/buildContents'; 11 | import { sentenceCase, camelCase } from 'change-case'; 12 | 13 | jest.mock('my/buildContents', () => ({ buildContents: jest.fn() })); 14 | 15 | const EXPECTED_TARGETS = [ 16 | { name: 'AppPage', value: 'lightning__AppPage' }, 17 | { name: 'HomePage', value: 'lightning__HomePage' }, 18 | { name: 'RecordPage', value: 'lightning__RecordPage' }, 19 | { name: 'RecordAction', value: 'lightning__RecordAction' }, 20 | { name: 'UtilityBar', value: 'lightning__UtilityBar' }, 21 | { name: 'FlowScreen', value: 'lightning__FlowScreen' }, 22 | { name: 'Tab', value: 'lightning__Tab' }, 23 | { name: 'Inbox', value: 'lightning__Inbox' }, 24 | { name: 'CommunityPage', value: 'lightningCommunity__Page' }, 25 | { name: 'CommunityDefault', value: 'lightningCommunity__Default' }, 26 | { name: 'CommunityPageLayout', value: 'lightningCommunity__Page_Layout' }, 27 | { name: 'CommunityThemeLayout', value: 'lightningCommunity__Theme_Layout' }, 28 | { name: 'SnapinChatMessage', value: 'lightningSnapin__ChatMessage' }, 29 | { name: 'SnapinMinimized', value: 'lightningSnapin__Minimized' }, 30 | { name: 'SnapinPreChat', value: 'lightningSnapin__PreChat' }, 31 | { name: 'SnapinChatHeader', value: 'lightningSnapin__ChatHeader' }, 32 | { 33 | name: 'Account Engagement (Pardot) Email', 34 | value: 'lightningStatic__Email' 35 | }, 36 | { name: 'CRM Analytics dashboard', value: 'analytics__Dashboard' }, 37 | { name: 'VoiceExtension', value: 'lightning__VoiceExtension' }, 38 | { name: 'EnablementProgram', value: 'lightning__EnablementProgram' }, 39 | { name: 'UrlAddressable', value: 'lightning__UrlAddressable' } 40 | ]; 41 | 42 | const EXPECTED_INPUTS = { 43 | componentName: '', 44 | apiVersion: '61.0', 45 | withHtml: true, 46 | withCss: true, 47 | withSvg: false, 48 | withTest: false, 49 | isExposed: true, 50 | masterLabel: '', 51 | description: '', 52 | configurationEditor: '', 53 | svgFileName: '', 54 | svgFileContent: '', 55 | targets: {}, 56 | propertyIds: ['propertyId_0'], 57 | properties: [], 58 | objectIds: ['objectId_0'], 59 | objects: [] 60 | }; 61 | 62 | EXPECTED_TARGETS.forEach((t) => { 63 | EXPECTED_INPUTS.targets[t.value] = { 64 | name: t.name, 65 | value: t.value, 66 | enabled: false, 67 | small: false, 68 | large: false, 69 | headlessAction: false, 70 | hasStep: false, 71 | properties: [], 72 | objects: [] 73 | }; 74 | }); 75 | 76 | describe('my-form', () => { 77 | afterEach(() => { 78 | // The jsdom instance is shared across test cases in a single file so reset the DOM 79 | while (document.body.firstChild) { 80 | document.body.removeChild(document.body.firstChild); 81 | } 82 | }); 83 | 84 | it('updates content when isExposed changed', () => { 85 | // GIVEN 86 | const element = createElement('my-form', { 87 | is: MyForm 88 | }); 89 | document.body.appendChild(element); 90 | 91 | const input = element.shadowRoot.querySelector('input[name="isExposed"]'); 92 | input.checked = true; 93 | input.dispatchEvent(new CustomEvent('change')); 94 | 95 | // Return a promise to wait for any asynchronous DOM updates. Jest 96 | // will automatically wait for the Promise chain to complete before 97 | // ending the test and fail the test if the promise rejects. 98 | return Promise.resolve().then(() => { 99 | // THEN 100 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 101 | expectedInputs.isExposed = true; 102 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 103 | }); 104 | }); 105 | 106 | it('updates content when withHtml changed', () => { 107 | // GIVEN 108 | const element = createElement('my-form', { 109 | is: MyForm 110 | }); 111 | document.body.appendChild(element); 112 | 113 | const input = element.shadowRoot.querySelector('input[name="withHtml"]'); 114 | input.checked = true; 115 | input.dispatchEvent(new CustomEvent('change')); 116 | 117 | // Return a promise to wait for any asynchronous DOM updates. Jest 118 | // will automatically wait for the Promise chain to complete before 119 | // ending the test and fail the test if the promise rejects. 120 | return Promise.resolve().then(() => { 121 | // THEN 122 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 123 | expectedInputs.withHtml = true; 124 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 125 | }); 126 | }); 127 | 128 | it('updates content when withCss changed', () => { 129 | // GIVEN 130 | const element = createElement('my-form', { 131 | is: MyForm 132 | }); 133 | document.body.appendChild(element); 134 | 135 | const input = element.shadowRoot.querySelector('input[name="withCss"]'); 136 | input.checked = true; 137 | input.dispatchEvent(new CustomEvent('change')); 138 | 139 | // Return a promise to wait for any asynchronous DOM updates. Jest 140 | // will automatically wait for the Promise chain to complete before 141 | // ending the test and fail the test if the promise rejects. 142 | return Promise.resolve().then(() => { 143 | // THEN 144 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 145 | expectedInputs.withCss = true; 146 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 147 | }); 148 | }); 149 | 150 | it('updates content when withSvg changed', () => { 151 | // GIVEN 152 | const element = createElement('my-form', { 153 | is: MyForm 154 | }); 155 | document.body.appendChild(element); 156 | 157 | const input = element.shadowRoot.querySelector('input[name="withSvg"]'); 158 | input.checked = true; 159 | input.dispatchEvent(new CustomEvent('change')); 160 | 161 | // Return a promise to wait for any asynchronous DOM updates. Jest 162 | // will automatically wait for the Promise chain to complete before 163 | // ending the test and fail the test if the promise rejects. 164 | return Promise.resolve().then(() => { 165 | // THEN 166 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 167 | expectedInputs.withSvg = true; 168 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 169 | }); 170 | }); 171 | 172 | it('updates content when withTest changed', () => { 173 | // GIVEN 174 | const element = createElement('my-form', { 175 | is: MyForm 176 | }); 177 | document.body.appendChild(element); 178 | 179 | const input = element.shadowRoot.querySelector('input[name="withTest"]'); 180 | input.checked = true; 181 | input.dispatchEvent(new CustomEvent('change')); 182 | 183 | // Return a promise to wait for any asynchronous DOM updates. Jest 184 | // will automatically wait for the Promise chain to complete before 185 | // ending the test and fail the test if the promise rejects. 186 | return Promise.resolve().then(() => { 187 | // THEN 188 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 189 | expectedInputs.withTest = true; 190 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 191 | }); 192 | }); 193 | 194 | it('updates content when component name input changed', () => { 195 | // GIVEN 196 | const element = createElement('my-form', { 197 | is: MyForm 198 | }); 199 | document.body.appendChild(element); 200 | 201 | // WHEN 202 | const input = element.shadowRoot.querySelector( 203 | 'input[name="componentName"]' 204 | ); 205 | input.value = 'MyCmp'; 206 | input.dispatchEvent(new CustomEvent('change')); 207 | 208 | // THEN 209 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 210 | expectedInputs.componentName = camelCase(input.value); 211 | expectedInputs.masterLabel = sentenceCase(input.value); 212 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 213 | }); 214 | 215 | it('updates content when master label input changed', () => { 216 | // GIVEN 217 | const element = createElement('my-form', { 218 | is: MyForm 219 | }); 220 | document.body.appendChild(element); 221 | 222 | // WHEN 223 | const input = element.shadowRoot.querySelector('input[name="masterLabel"]'); 224 | input.value = 'My Primary Label'; 225 | input.dispatchEvent(new CustomEvent('change')); 226 | 227 | // THEN 228 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 229 | expectedInputs.masterLabel = input.value; 230 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 231 | }); 232 | 233 | it('updates content when description input changed', () => { 234 | // GIVEN 235 | const element = createElement('my-form', { 236 | is: MyForm 237 | }); 238 | document.body.appendChild(element); 239 | 240 | // WHEN 241 | const input = element.shadowRoot.querySelector('input[name="description"]'); 242 | input.value = 'My Desc'; 243 | input.dispatchEvent(new CustomEvent('change')); 244 | 245 | // THEN 246 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 247 | expectedInputs.description = input.value; 248 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 249 | }); 250 | 251 | it('updates content when config editor input changed', () => { 252 | // GIVEN 253 | const element = createElement('my-form', { 254 | is: MyForm 255 | }); 256 | document.body.appendChild(element); 257 | 258 | const COMP_NAME = 'MyFlowCmp'; 259 | 260 | // WHEN 261 | const cmpName = element.shadowRoot.querySelector( 262 | 'input[name="componentName"]' 263 | ); 264 | cmpName.value = COMP_NAME; 265 | cmpName.dispatchEvent(new CustomEvent('change')); 266 | 267 | const target = element.shadowRoot.querySelector('my-target-definition'); 268 | const event = new CustomEvent('changetarget', { 269 | detail: { 270 | target: { value: 'lightning__FlowScreen' }, 271 | enabled: true, 272 | small: false, 273 | large: false, 274 | headlessAction: false, 275 | hasStep: false 276 | } 277 | }); 278 | target.dispatchEvent(event); 279 | 280 | const CONFIG_EDITOR_NAME = 'My Name in Flow'; 281 | 282 | // Return a promise to wait for any asynchronous DOM updates. Jest 283 | // will automatically wait for the Promise chain to complete before 284 | // ending the test and fail the test if the promise rejects. 285 | return Promise.resolve() 286 | .then(() => { 287 | // WHEN 288 | const configEditorInput = element.shadowRoot.querySelector( 289 | 'input[name="configurationEditor"]' 290 | ); 291 | configEditorInput.value = CONFIG_EDITOR_NAME; 292 | configEditorInput.dispatchEvent(new CustomEvent('change')); 293 | }) 294 | .then(() => { 295 | // THEN 296 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 297 | expectedInputs.componentName = camelCase(cmpName.value); 298 | expectedInputs.masterLabel = sentenceCase(cmpName.value); 299 | expectedInputs.configurationEditor = CONFIG_EDITOR_NAME; 300 | expectedInputs.targets.lightning__FlowScreen.enabled = true; 301 | expectedInputs.targets.lightning__FlowScreen.enabled = true; 302 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 303 | }); 304 | }); 305 | 306 | it('updates content when svg input changed', () => { 307 | // GIVEN 308 | const element = createElement('my-form', { 309 | is: MyForm 310 | }); 311 | document.body.appendChild(element); 312 | 313 | const input = element.shadowRoot.querySelector('input[name="withSvg"]'); 314 | input.checked = true; 315 | input.dispatchEvent(new CustomEvent('change')); 316 | 317 | // Return a promise to wait for any asynchronous DOM updates. Jest 318 | // will automatically wait for the Promise chain to complete before 319 | // ending the test and fail the test if the promise rejects. 320 | return Promise.resolve().then(() => { 321 | // WHEN 322 | const svgUploader = element.shadowRoot.querySelector('my-svg-uploader'); 323 | svgUploader.dispatchEvent( 324 | new CustomEvent('uploadsvg', { 325 | detail: { fileName: 'myFileName', fileContent: 'myFileContent' } 326 | }) 327 | ); 328 | 329 | // THEN 330 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 331 | expectedInputs.svgFileName = 'myFileName'; 332 | expectedInputs.svgFileContent = 'myFileContent'; 333 | expectedInputs.withSvg = true; 334 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 335 | }); 336 | }); 337 | 338 | it('fires updatecontent event when inputs change', () => { 339 | // GIVEN 340 | const element = createElement('my-form', { 341 | is: MyForm 342 | }); 343 | document.body.appendChild(element); 344 | 345 | const doSomething = jest.fn(); 346 | element.addEventListener('updatecontent', doSomething); 347 | 348 | // WHEN 349 | const input = element.shadowRoot.querySelector('input[name="description"]'); 350 | input.value = 'My Desc'; 351 | input.dispatchEvent(new CustomEvent('change')); 352 | 353 | // THEN 354 | expect(doSomething).toHaveBeenCalled(); 355 | }); 356 | 357 | it('renders property targets when target selected', () => { 358 | // GIVEN 359 | const element = createElement('my-form', { 360 | is: MyForm 361 | }); 362 | document.body.appendChild(element); 363 | 364 | // WHEN 365 | const target = element.shadowRoot.querySelector('my-target-definition'); 366 | const event = new CustomEvent('changetarget', { 367 | detail: { target: { value: 'lightning__AppPage' }, enabled: true } 368 | }); 369 | target.dispatchEvent(event); 370 | 371 | // THEN 372 | // Return a promise to wait for any asynchronous DOM updates. Jest 373 | // will automatically wait for the Promise chain to complete before 374 | // ending the test and fail the test if the promise rejects. 375 | return Promise.resolve().then(() => { 376 | const propertyDefinitions = element.shadowRoot.querySelectorAll( 377 | 'my-property-definition' 378 | ); 379 | expect(propertyDefinitions.length).toBe(1); 380 | EXPECTED_TARGETS.forEach((item) => { 381 | expect(item.value in propertyDefinitions[0].targets).toBe(true); 382 | }); 383 | expect(propertyDefinitions[0].pid).toBe('propertyId_0'); 384 | }); 385 | }); 386 | 387 | it('renders objects when record page selected', () => { 388 | // GIVEN 389 | const element = createElement('my-form', { 390 | is: MyForm 391 | }); 392 | document.body.appendChild(element); 393 | 394 | // WHEN 395 | const target = element.shadowRoot.querySelector('my-target-definition'); 396 | const event = new CustomEvent('changetarget', { 397 | detail: { target: { value: 'lightning__RecordPage' }, enabled: true } 398 | }); 399 | target.dispatchEvent(event); 400 | 401 | // THEN 402 | // Return a promise to wait for any asynchronous DOM updates. Jest 403 | // will automatically wait for the Promise chain to complete before 404 | // ending the test and fail the test if the promise rejects. 405 | return Promise.resolve().then(() => { 406 | const sobjectDefinitions = element.shadowRoot.querySelectorAll( 407 | 'my-sobject-definition' 408 | ); 409 | expect(sobjectDefinitions.length).toBe(1); 410 | expect(sobjectDefinitions[0].oid).toBe('objectId_0'); 411 | }); 412 | }); 413 | 414 | it('adds property row when button clicked', () => { 415 | // GIVEN 416 | const element = createElement('my-form', { 417 | is: MyForm 418 | }); 419 | document.body.appendChild(element); 420 | 421 | const target = element.shadowRoot.querySelector('my-target-definition'); 422 | const event = new CustomEvent('changetarget', { 423 | detail: { target: { value: 'lightning__RecordPage' }, enabled: true } 424 | }); 425 | target.dispatchEvent(event); 426 | 427 | // THEN 428 | // Return a promise to wait for any asynchronous DOM updates. Jest 429 | // will automatically wait for the Promise chain to complete before 430 | // ending the test and fail the test if the promise rejects. 431 | return Promise.resolve().then(() => { 432 | const button = element.shadowRoot.querySelector( 433 | 'button[name="addPropertyRow"]' 434 | ); 435 | button.dispatchEvent(new CustomEvent('click')); 436 | 437 | return Promise.resolve().then(() => { 438 | const propertyDefinitions = element.shadowRoot.querySelectorAll( 439 | 'my-property-definition' 440 | ); 441 | expect(propertyDefinitions.length).toBe(2); 442 | expect(propertyDefinitions[0].pid).toBe('propertyId_0'); 443 | expect(propertyDefinitions[1].pid).toBe('propertyId_1'); 444 | }); 445 | }); 446 | }); 447 | 448 | it('removes property row when button clicked', () => { 449 | // GIVEN 450 | const element = createElement('my-form', { 451 | is: MyForm 452 | }); 453 | document.body.appendChild(element); 454 | 455 | const target = element.shadowRoot.querySelector('my-target-definition'); 456 | const event = new CustomEvent('changetarget', { 457 | detail: { target: { value: 'lightning__RecordPage' }, enabled: true } 458 | }); 459 | target.dispatchEvent(event); 460 | 461 | // THEN 462 | // Return a promise to wait for any asynchronous DOM updates. Jest 463 | // will automatically wait for the Promise chain to complete before 464 | // ending the test and fail the test if the promise rejects. 465 | return Promise.resolve().then(() => { 466 | const propertyDefinition = element.shadowRoot.querySelector( 467 | 'my-property-definition' 468 | ); 469 | propertyDefinition.dispatchEvent( 470 | new CustomEvent('deletepropdef', { detail: 'propertyId_0' }) 471 | ); 472 | 473 | return Promise.resolve().then(() => { 474 | const propertyDefinitions = element.shadowRoot.querySelectorAll( 475 | 'my-property-definition' 476 | ); 477 | expect(propertyDefinitions.length).toBe(0); 478 | }); 479 | }); 480 | }); 481 | 482 | it('updates content when property row event listened', () => { 483 | // GIVEN 484 | const element = createElement('my-form', { 485 | is: MyForm 486 | }); 487 | document.body.appendChild(element); 488 | 489 | const target = element.shadowRoot.querySelector('my-target-definition'); 490 | const event = new CustomEvent('changetarget', { 491 | detail: { 492 | target: { value: 'lightning__RecordPage' }, 493 | enabled: true, 494 | small: true, 495 | large: false, 496 | headlessAction: false, 497 | hasStep: false 498 | } 499 | }); 500 | target.dispatchEvent(event); 501 | 502 | // THEN 503 | // Return a promise to wait for any asynchronous DOM updates. Jest 504 | // will automatically wait for the Promise chain to complete before 505 | // ending the test and fail the test if the promise rejects. 506 | return Promise.resolve().then(() => { 507 | const propertyDefinition = element.shadowRoot.querySelector( 508 | 'my-property-definition' 509 | ); 510 | propertyDefinition.dispatchEvent( 511 | new CustomEvent('changepropdef', { 512 | detail: { id: 'propertyId_0' } 513 | }) 514 | ); 515 | 516 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 517 | expectedInputs.properties.push({ id: 'propertyId_0' }); 518 | expectedInputs.targets.lightning__RecordPage.small = true; 519 | expectedInputs.targets.lightning__RecordPage.enabled = true; 520 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 521 | }); 522 | }); 523 | 524 | it('adds object row when button clicked', () => { 525 | // GIVEN 526 | const element = createElement('my-form', { 527 | is: MyForm 528 | }); 529 | document.body.appendChild(element); 530 | 531 | const target = element.shadowRoot.querySelector('my-target-definition'); 532 | const event = new CustomEvent('changetarget', { 533 | detail: { target: { value: 'lightning__RecordPage' }, enabled: true } 534 | }); 535 | target.dispatchEvent(event); 536 | 537 | // THEN 538 | // Return a promise to wait for any asynchronous DOM updates. Jest 539 | // will automatically wait for the Promise chain to complete before 540 | // ending the test and fail the test if the promise rejects. 541 | return Promise.resolve().then(() => { 542 | const button = element.shadowRoot.querySelector( 543 | 'button[name="addObjectRow"]' 544 | ); 545 | button.dispatchEvent(new CustomEvent('click')); 546 | 547 | return Promise.resolve().then(() => { 548 | const objects = element.shadowRoot.querySelectorAll( 549 | 'my-sobject-definition' 550 | ); 551 | expect(objects.length).toBe(2); 552 | expect(objects[0].oid).toBe('objectId_0'); 553 | expect(objects[1].oid).toBe('objectId_1'); 554 | }); 555 | }); 556 | }); 557 | 558 | it('removes object row when button clicked', () => { 559 | // GIVEN 560 | const element = createElement('my-form', { 561 | is: MyForm 562 | }); 563 | document.body.appendChild(element); 564 | 565 | const target = element.shadowRoot.querySelector('my-target-definition'); 566 | const event = new CustomEvent('changetarget', { 567 | detail: { target: { value: 'lightning__RecordPage' }, enabled: true } 568 | }); 569 | target.dispatchEvent(event); 570 | 571 | // THEN 572 | // Return a promise to wait for any asynchronous DOM updates. Jest 573 | // will automatically wait for the Promise chain to complete before 574 | // ending the test and fail the test if the promise rejects. 575 | return Promise.resolve().then(() => { 576 | const object = element.shadowRoot.querySelector('my-sobject-definition'); 577 | object.dispatchEvent( 578 | new CustomEvent('deletesobj', { detail: 'objectId_0' }) 579 | ); 580 | 581 | return Promise.resolve().then(() => { 582 | const objects = element.shadowRoot.querySelectorAll( 583 | 'my-sobject-definition' 584 | ); 585 | expect(objects.length).toBe(0); 586 | }); 587 | }); 588 | }); 589 | 590 | it('updates content when object row event listened', () => { 591 | // GIVEN 592 | const element = createElement('my-form', { 593 | is: MyForm 594 | }); 595 | document.body.appendChild(element); 596 | 597 | const target = element.shadowRoot.querySelector('my-target-definition'); 598 | const event = new CustomEvent('changetarget', { 599 | detail: { 600 | target: { value: 'lightning__RecordPage' }, 601 | enabled: true, 602 | small: false, 603 | large: false, 604 | headlessAction: false, 605 | hasStep: false 606 | } 607 | }); 608 | target.dispatchEvent(event); 609 | 610 | // THEN 611 | // Return a promise to wait for any asynchronous DOM updates. Jest 612 | // will automatically wait for the Promise chain to complete before 613 | // ending the test and fail the test if the promise rejects. 614 | return Promise.resolve().then(() => { 615 | const object = element.shadowRoot.querySelector('my-sobject-definition'); 616 | object.dispatchEvent( 617 | new CustomEvent('changesobj', { 618 | detail: { id: 'objectId_0' } 619 | }) 620 | ); 621 | 622 | const expectedInputs = JSON.parse(JSON.stringify(EXPECTED_INPUTS)); 623 | expectedInputs.objects.push({ id: 'objectId_0' }); 624 | expectedInputs.targets.lightning__RecordPage.enabled = true; 625 | expect(buildContents).toHaveBeenLastCalledWith(expectedInputs); 626 | }); 627 | }); 628 | }); 629 | -------------------------------------------------------------------------------- /src/modules/my/form/form.css: -------------------------------------------------------------------------------- 1 | .target-section .slds-checkbox { 2 | display: flex; 3 | align-items: center; 4 | } 5 | .target-section .target_label { 6 | flex: 1; 7 | } 8 | 9 | .target-section .slds-col { 10 | padding-top: 0.5rem; 11 | } 12 | hr { 13 | margin: 1rem 0; 14 | } 15 | .header-row { 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | } 20 | .header-row .slds-text-heading_large { 21 | flex: 1; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/my/form/form.html: -------------------------------------------------------------------------------- 1 | 409 | -------------------------------------------------------------------------------- /src/modules/my/form/form.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, track } from 'lwc'; 8 | import { buildContents } from 'my/buildContents'; 9 | import { sentenceCase, camelCase } from 'change-case'; 10 | 11 | export default class Form extends LightningElement { 12 | componentName = ''; 13 | 14 | @track 15 | inputs = { 16 | componentName: '', 17 | apiVersion: '61.0', 18 | withHtml: true, 19 | withCss: true, 20 | withSvg: false, 21 | withTest: false, 22 | isExposed: true, 23 | masterLabel: '', 24 | description: '', 25 | configurationEditor: '', 26 | svgFileName: '', 27 | svgFileContent: '', 28 | targets: {}, 29 | propertyIds: [], 30 | properties: [], 31 | objectIds: [], 32 | objects: [] 33 | }; 34 | 35 | targetsArr = [ 36 | { name: 'AppPage', value: 'lightning__AppPage' }, 37 | { name: 'HomePage', value: 'lightning__HomePage' }, 38 | { name: 'RecordPage', value: 'lightning__RecordPage' }, 39 | { name: 'RecordAction', value: 'lightning__RecordAction' }, 40 | { name: 'UtilityBar', value: 'lightning__UtilityBar' }, 41 | { name: 'FlowScreen', value: 'lightning__FlowScreen' }, 42 | { name: 'Tab', value: 'lightning__Tab' }, 43 | { name: 'Inbox', value: 'lightning__Inbox' }, 44 | { name: 'CommunityPage', value: 'lightningCommunity__Page' }, 45 | { name: 'CommunityDefault', value: 'lightningCommunity__Default' }, 46 | { name: 'CommunityPageLayout', value: 'lightningCommunity__Page_Layout' }, 47 | { name: 'CommunityThemeLayout', value: 'lightningCommunity__Theme_Layout' }, 48 | { name: 'SnapinChatMessage', value: 'lightningSnapin__ChatMessage' }, 49 | { name: 'SnapinMinimized', value: 'lightningSnapin__Minimized' }, 50 | { name: 'SnapinPreChat', value: 'lightningSnapin__PreChat' }, 51 | { name: 'SnapinChatHeader', value: 'lightningSnapin__ChatHeader' }, 52 | { 53 | name: 'Account Engagement (Pardot) Email', 54 | value: 'lightningStatic__Email' 55 | }, 56 | { name: 'CRM Analytics dashboard', value: 'analytics__Dashboard' }, 57 | { name: 'VoiceExtension', value: 'lightning__VoiceExtension' }, 58 | { name: 'EnablementProgram', value: 'lightning__EnablementProgram' }, 59 | { name: 'UrlAddressable', value: 'lightning__UrlAddressable' } 60 | ]; 61 | pDefCount = 0; 62 | oDefCount = 0; 63 | 64 | connectedCallback() { 65 | this.targetsArr.forEach((t) => { 66 | this.inputs.targets[t.value] = { 67 | name: t.name, 68 | value: t.value, 69 | enabled: false, 70 | small: false, 71 | large: false, 72 | headlessAction: false, 73 | hasStep: false, 74 | properties: [], 75 | objects: [] 76 | }; 77 | }); 78 | 79 | this.addObjectRow(); 80 | this.addPropertyRow(); 81 | } 82 | 83 | onChangeInput(e) { 84 | const formattedValue = this.formatInputValue( 85 | e.currentTarget.name, 86 | e.currentTarget.value 87 | ); 88 | this.inputs[e.currentTarget.name] = formattedValue; 89 | if (e.currentTarget.name === 'componentName' && !this.inputs.masterLabel) { 90 | this.inputs.masterLabel = sentenceCase(formattedValue); 91 | } 92 | this.updateContent(); 93 | } 94 | 95 | onChangeSelect(e) { 96 | this.inputs[e.currentTarget.name] = e.currentTarget.value; 97 | this.updateContent(); 98 | } 99 | 100 | onChangeCheckbox(e) { 101 | this.inputs[e.currentTarget.name] = e.currentTarget.checked; 102 | this.updateContent(); 103 | } 104 | 105 | onChangeTargetValue(e) { 106 | if (!e.detail) { 107 | return; 108 | } 109 | const { target, enabled, small, large, headlessAction, hasStep } = e.detail; 110 | 111 | this.inputs.targets[target.value].enabled = enabled; 112 | if (enabled) { 113 | this.inputs.targets[target.value].small = small; 114 | this.inputs.targets[target.value].large = large; 115 | this.inputs.targets[target.value].headlessAction = headlessAction; 116 | this.inputs.targets[target.value].hasStep = hasStep; 117 | } 118 | // remove check of properties which targets are disabled. 119 | if (!enabled) { 120 | this.inputs.properties.forEach((p) => { 121 | const si = p.selectedTargets.findIndex((st) => st === target.value); 122 | if (si >= 0) { 123 | p.selectedTargets.splice(si, 1); 124 | } 125 | }); 126 | } 127 | 128 | this.updateContent(); 129 | } 130 | 131 | formatInputValue = (key, value) => { 132 | if (key === 'componentName') { 133 | return camelCase(value.replace(/^\d+/, '')); 134 | } 135 | return value; 136 | }; 137 | 138 | get isRecordPageSelected() { 139 | return this.inputs.targets.lightning__RecordPage.enabled === true; 140 | } 141 | 142 | addPropertyRow = () => { 143 | const pId = `propertyId_${this.pDefCount}`; 144 | this.inputs.propertyIds.push(pId); 145 | this.pDefCount++; 146 | }; 147 | 148 | deletePropertyRow = (e) => { 149 | const targetId = e.detail; 150 | this.inputs.propertyIds = this.inputs.propertyIds.filter( 151 | (pId) => pId !== targetId 152 | ); 153 | this.inputs.properties = this.inputs.properties.filter( 154 | (p) => p.id !== targetId 155 | ); 156 | this.updateContent(); 157 | }; 158 | 159 | addObjectRow = () => { 160 | const oId = `objectId_${this.oDefCount}`; 161 | this.inputs.objectIds.push(oId); 162 | // this.inputs.objects.push({ id: oId }); 163 | this.oDefCount++; 164 | }; 165 | 166 | deleteObjectRow = (e) => { 167 | const targetId = e.detail; 168 | this.inputs.objectIds = this.inputs.objectIds.filter( 169 | (oId) => oId !== targetId 170 | ); 171 | this.inputs.objects = this.inputs.objects.filter((o) => o.id !== targetId); 172 | // update content when the object is deleted from config 173 | this.updateContent(); 174 | }; 175 | 176 | onChangePropertyRow = (e) => { 177 | const pIndex = this.inputs.properties.findIndex( 178 | (p) => p.id === e.detail.id 179 | ); 180 | if (pIndex === -1) { 181 | this.inputs.properties.push(e.detail); 182 | } else { 183 | this.inputs.properties[pIndex] = e.detail; 184 | } 185 | this.updateContent(); 186 | }; 187 | 188 | onChangeObjectRow = (e) => { 189 | const oIndex = this.inputs.objects.findIndex((o) => o.id === e.detail.id); 190 | if (oIndex === -1) { 191 | this.inputs.objects.push(e.detail); 192 | } else { 193 | this.inputs.objects[oIndex] = e.detail; 194 | } 195 | this.updateContent(); 196 | }; 197 | 198 | updateContent() { 199 | const formatted = buildContents(this.inputs); 200 | const e = new CustomEvent('updatecontent', { 201 | detail: formatted 202 | }); 203 | this.dispatchEvent(e); 204 | } 205 | 206 | handleSvgUpload = (e) => { 207 | const { fileName, fileContent } = e.detail; 208 | this.inputs.svgFileName = fileName; 209 | this.inputs.svgFileContent = fileContent; 210 | this.updateContent(); 211 | }; 212 | 213 | nonPropertyTargets = [ 214 | 'lightning__Tab', 215 | 'lightningSnapin__ChatMessage', 216 | 'lightningCommunity__Page', 217 | 'lightningSnapin__Minimized', 218 | 'lightningSnapin__PreChat', 219 | 'lightningSnapin__ChatHeader' 220 | ]; 221 | 222 | get hasPropertyTarget() { 223 | return ( 224 | Object.values(this.inputs.targets) 225 | .filter((t) => t.enabled) 226 | .filter((t) => { 227 | return !this.nonPropertyTargets.includes(t.value); 228 | }).length > 0 229 | ); 230 | } 231 | 232 | get isFlowOnly() { 233 | const enabledTargets = Object.values(this.inputs.targets).filter( 234 | (t) => t.enabled 235 | ); 236 | return ( 237 | enabledTargets.length === 1 && 238 | enabledTargets[0].value === 'lightning__FlowScreen' 239 | ); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/modules/my/preview/__tests__/preview.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { createElement } from 'lwc'; 9 | import MyPreview from 'my/preview'; 10 | 11 | describe('my-preview', () => { 12 | afterEach(() => { 13 | // The jsdom instance is shared across test cases in a single file so reset the DOM 14 | while (document.body.firstChild) { 15 | document.body.removeChild(document.body.firstChild); 16 | } 17 | }); 18 | 19 | it('shows html preview header when withHtml', () => { 20 | // GIVEN 21 | const element = createElement('my-preview', { 22 | is: MyPreview 23 | }); 24 | element.contents = { withHtml: true, componentName: 'MyLWC' }; 25 | document.body.appendChild(element); 26 | 27 | // WHEN 28 | const previewHeader = element.shadowRoot.querySelector( 29 | 'my-preview-header.html-header' 30 | ); 31 | 32 | // THEN 33 | expect(previewHeader.type).toBe('html'); 34 | expect(previewHeader.filename).toBe(element.contents.componentName); 35 | }); 36 | 37 | it('always shows js preview header', () => { 38 | // GIVEN 39 | const element = createElement('my-preview', { 40 | is: MyPreview 41 | }); 42 | element.contents = { componentName: 'MyLWC' }; 43 | document.body.appendChild(element); 44 | 45 | // WHEN 46 | const previewHeader = element.shadowRoot.querySelector( 47 | 'my-preview-header.js-header' 48 | ); 49 | 50 | // THEN 51 | expect(previewHeader.type).toBe('js'); 52 | expect(previewHeader.filename).toBe(element.contents.componentName); 53 | }); 54 | 55 | it('always shows meta preview header', () => { 56 | // GIVEN 57 | const element = createElement('my-preview', { 58 | is: MyPreview 59 | }); 60 | element.contents = { componentName: 'MyLWC' }; 61 | document.body.appendChild(element); 62 | 63 | // WHEN 64 | const previewHeader = element.shadowRoot.querySelector( 65 | 'my-preview-header.meta-header' 66 | ); 67 | 68 | // THEN 69 | expect(previewHeader.type).toBe('meta'); 70 | expect(previewHeader.filename).toBe(element.contents.componentName); 71 | }); 72 | 73 | it('shows css preview header when withCss', () => { 74 | // GIVEN 75 | const element = createElement('my-preview', { 76 | is: MyPreview 77 | }); 78 | element.contents = { withCss: true, componentName: 'MyLWC' }; 79 | document.body.appendChild(element); 80 | 81 | // WHEN 82 | const previewHeader = element.shadowRoot.querySelector( 83 | 'my-preview-header.css-header' 84 | ); 85 | 86 | // THEN 87 | expect(previewHeader.type).toBe('css'); 88 | expect(previewHeader.filename).toBe(element.contents.componentName); 89 | }); 90 | 91 | it('shows svg preview header when withSvg', () => { 92 | // GIVEN 93 | const element = createElement('my-preview', { 94 | is: MyPreview 95 | }); 96 | element.contents = { withSvg: true, componentName: 'MyLWC' }; 97 | document.body.appendChild(element); 98 | 99 | // WHEN 100 | const previewHeader = element.shadowRoot.querySelector( 101 | 'my-preview-header.svg-header' 102 | ); 103 | 104 | // THEN 105 | expect(previewHeader.type).toBe('svg'); 106 | expect(previewHeader.filename).toBe(element.contents.componentName); 107 | }); 108 | 109 | it('shows test preview header when withTest', () => { 110 | // GIVEN 111 | const element = createElement('my-preview', { 112 | is: MyPreview 113 | }); 114 | element.contents = { withTest: true, componentName: 'MyLWC' }; 115 | document.body.appendChild(element); 116 | 117 | // WHEN 118 | const previewHeader = element.shadowRoot.querySelector( 119 | 'my-preview-header.test-header' 120 | ); 121 | 122 | // THEN 123 | expect(previewHeader.type).toBe('test'); 124 | expect(previewHeader.filename).toBe(element.contents.componentName); 125 | }); 126 | 127 | it('shows html preview content when withHtml', () => { 128 | // GIVEN 129 | const element = createElement('my-preview', { 130 | is: MyPreview 131 | }); 132 | element.contents = { withHtml: true, componentName: 'MyLWC', html: 'html' }; 133 | document.body.appendChild(element); 134 | 135 | // WHEN 136 | const previewContent = element.shadowRoot.querySelector( 137 | 'my-preview-content.html' 138 | ); 139 | 140 | // THEN 141 | expect(previewContent.type).toBe('html'); 142 | expect(previewContent.filename).toBe(element.contents.componentName); 143 | expect(previewContent.content).toBe(element.contents.html); 144 | }); 145 | 146 | it('always shows js preview content', () => { 147 | // GIVEN 148 | const element = createElement('my-preview', { 149 | is: MyPreview 150 | }); 151 | element.contents = { componentName: 'MyLWC', js: 'js' }; 152 | document.body.appendChild(element); 153 | 154 | // WHEN 155 | const previewContent = element.shadowRoot.querySelector( 156 | 'my-preview-content.js' 157 | ); 158 | 159 | // THEN 160 | expect(previewContent.type).toBe('js'); 161 | expect(previewContent.filename).toBe(element.contents.componentName); 162 | expect(previewContent.content).toBe(element.contents.js); 163 | }); 164 | 165 | it('always shows meta preview content', () => { 166 | // GIVEN 167 | const element = createElement('my-preview', { 168 | is: MyPreview 169 | }); 170 | element.contents = { componentName: 'MyLWC', meta: 'meta' }; 171 | document.body.appendChild(element); 172 | 173 | // WHEN 174 | const previewContent = element.shadowRoot.querySelector( 175 | 'my-preview-content.meta' 176 | ); 177 | 178 | // THEN 179 | expect(previewContent.type).toBe('meta'); 180 | expect(previewContent.filename).toBe(element.contents.componentName); 181 | expect(previewContent.content).toBe(element.contents.meta); 182 | }); 183 | 184 | it('shows css preview content when withCss', () => { 185 | // GIVEN 186 | const element = createElement('my-preview', { 187 | is: MyPreview 188 | }); 189 | element.contents = { withCss: true, componentName: 'MyLWC', css: 'css' }; 190 | document.body.appendChild(element); 191 | 192 | // WHEN 193 | const previewContent = element.shadowRoot.querySelector( 194 | 'my-preview-content.css' 195 | ); 196 | 197 | // THEN 198 | expect(previewContent.type).toBe('css'); 199 | expect(previewContent.filename).toBe(element.contents.componentName); 200 | expect(previewContent.content).toBe(element.contents.css); 201 | }); 202 | 203 | it('shows svg preview content when withSvg', () => { 204 | // GIVEN 205 | const element = createElement('my-preview', { 206 | is: MyPreview 207 | }); 208 | element.contents = { withSvg: true, componentName: 'MyLWC', svg: 'svg' }; 209 | document.body.appendChild(element); 210 | 211 | // WHEN 212 | const previewContent = element.shadowRoot.querySelector( 213 | 'my-preview-content.svg' 214 | ); 215 | 216 | // THEN 217 | expect(previewContent.type).toBe('svg'); 218 | expect(previewContent.filename).toBe(element.contents.componentName); 219 | expect(previewContent.content).toBe(element.contents.svg); 220 | }); 221 | 222 | it('shows test preview content when withTest', () => { 223 | // GIVEN 224 | const element = createElement('my-preview', { 225 | is: MyPreview 226 | }); 227 | element.contents = { withTest: true, componentName: 'MyLWC', test: 'test' }; 228 | document.body.appendChild(element); 229 | 230 | // WHEN 231 | const previewContent = element.shadowRoot.querySelector( 232 | 'my-preview-content.test' 233 | ); 234 | 235 | // THEN 236 | expect(previewContent.type).toBe('test'); 237 | expect(previewContent.filename).toBe(element.contents.componentName); 238 | expect(previewContent.content).toBe(element.contents.test); 239 | }); 240 | 241 | it('shows html content when click on html header', () => { 242 | // GIVEN 243 | const element = createElement('my-preview', { 244 | is: MyPreview 245 | }); 246 | element.contents = { withHtml: true, componentName: 'MyLWC' }; 247 | document.body.appendChild(element); 248 | 249 | // WHEN 250 | const previewHeader = element.shadowRoot.querySelector( 251 | 'my-preview-header.html-header' 252 | ); 253 | previewHeader.dispatchEvent(new CustomEvent('clicktab')); 254 | 255 | // THEN 256 | // Return a promise to wait for any asynchronous DOM updates. Jest 257 | // will automatically wait for the Promise chain to complete before 258 | // ending the test and fail the test if the promise rejects. 259 | return Promise.resolve().then(() => { 260 | const previewContent = element.shadowRoot.querySelector( 261 | 'my-preview-content.html' 262 | ); 263 | expect(previewContent.classList.contains('preview-content')).toBe(true); 264 | expect(previewContent.classList.contains('selected')).toBe(true); 265 | }); 266 | }); 267 | 268 | it('shows js content when click on js header', () => { 269 | // GIVEN 270 | const element = createElement('my-preview', { 271 | is: MyPreview 272 | }); 273 | element.contents = { componentName: 'MyLWC' }; 274 | document.body.appendChild(element); 275 | 276 | // WHEN 277 | const previewHeader = element.shadowRoot.querySelector( 278 | 'my-preview-header.js-header' 279 | ); 280 | previewHeader.dispatchEvent(new CustomEvent('clicktab')); 281 | 282 | // THEN 283 | // Return a promise to wait for any asynchronous DOM updates. Jest 284 | // will automatically wait for the Promise chain to complete before 285 | // ending the test and fail the test if the promise rejects. 286 | return Promise.resolve().then(() => { 287 | const previewContent = element.shadowRoot.querySelector( 288 | 'my-preview-content.js' 289 | ); 290 | expect(previewContent.classList.contains('preview-content')).toBe(true); 291 | expect(previewContent.classList.contains('selected')).toBe(true); 292 | }); 293 | }); 294 | 295 | it('shows meta content when click on meta header', () => { 296 | // GIVEN 297 | const element = createElement('my-preview', { 298 | is: MyPreview 299 | }); 300 | element.contents = { componentName: 'MyLWC' }; 301 | document.body.appendChild(element); 302 | 303 | // WHEN 304 | const previewHeader = element.shadowRoot.querySelector( 305 | 'my-preview-header.meta-header' 306 | ); 307 | previewHeader.dispatchEvent(new CustomEvent('clicktab')); 308 | 309 | // THEN 310 | // Return a promise to wait for any asynchronous DOM updates. Jest 311 | // will automatically wait for the Promise chain to complete before 312 | // ending the test and fail the test if the promise rejects. 313 | return Promise.resolve().then(() => { 314 | const previewContent = element.shadowRoot.querySelector( 315 | 'my-preview-content.meta' 316 | ); 317 | expect(previewContent.classList.contains('preview-content')).toBe(true); 318 | expect(previewContent.classList.contains('selected')).toBe(true); 319 | }); 320 | }); 321 | 322 | it('shows css content when click on css header', () => { 323 | // GIVEN 324 | const element = createElement('my-preview', { 325 | is: MyPreview 326 | }); 327 | element.contents = { withCss: true, componentName: 'MyLWC' }; 328 | document.body.appendChild(element); 329 | 330 | // WHEN 331 | const previewHeader = element.shadowRoot.querySelector( 332 | 'my-preview-header.css-header' 333 | ); 334 | previewHeader.dispatchEvent(new CustomEvent('clicktab')); 335 | 336 | // THEN 337 | // Return a promise to wait for any asynchronous DOM updates. Jest 338 | // will automatically wait for the Promise chain to complete before 339 | // ending the test and fail the test if the promise rejects. 340 | return Promise.resolve().then(() => { 341 | const previewContent = element.shadowRoot.querySelector( 342 | 'my-preview-content.css' 343 | ); 344 | expect(previewContent.classList.contains('preview-content')).toBe(true); 345 | expect(previewContent.classList.contains('selected')).toBe(true); 346 | }); 347 | }); 348 | 349 | it('shows svg content when click on svg header', () => { 350 | // GIVEN 351 | const element = createElement('my-preview', { 352 | is: MyPreview 353 | }); 354 | element.contents = { withSvg: true, componentName: 'MyLWC' }; 355 | document.body.appendChild(element); 356 | 357 | // WHEN 358 | const previewHeader = element.shadowRoot.querySelector( 359 | 'my-preview-header.svg-header' 360 | ); 361 | previewHeader.dispatchEvent(new CustomEvent('clicktab')); 362 | 363 | // THEN 364 | // Return a promise to wait for any asynchronous DOM updates. Jest 365 | // will automatically wait for the Promise chain to complete before 366 | // ending the test and fail the test if the promise rejects. 367 | return Promise.resolve().then(() => { 368 | const previewContent = element.shadowRoot.querySelector( 369 | 'my-preview-content.svg' 370 | ); 371 | expect(previewContent.classList.contains('preview-content')).toBe(true); 372 | expect(previewContent.classList.contains('selected')).toBe(true); 373 | }); 374 | }); 375 | 376 | it('shows test content when click on test header', () => { 377 | // GIVEN 378 | const element = createElement('my-preview', { 379 | is: MyPreview 380 | }); 381 | element.contents = { withTest: true, componentName: 'MyLWC' }; 382 | document.body.appendChild(element); 383 | 384 | // WHEN 385 | const previewHeader = element.shadowRoot.querySelector( 386 | 'my-preview-header.test-header' 387 | ); 388 | previewHeader.dispatchEvent(new CustomEvent('clicktab')); 389 | 390 | // THEN 391 | // Return a promise to wait for any asynchronous DOM updates. Jest 392 | // will automatically wait for the Promise chain to complete before 393 | // ending the test and fail the test if the promise rejects. 394 | return Promise.resolve().then(() => { 395 | const previewContent = element.shadowRoot.querySelector( 396 | 'my-preview-content.test' 397 | ); 398 | expect(previewContent.classList.contains('preview-content')).toBe(true); 399 | expect(previewContent.classList.contains('selected')).toBe(true); 400 | }); 401 | }); 402 | }); 403 | -------------------------------------------------------------------------------- /src/modules/my/preview/preview.css: -------------------------------------------------------------------------------- 1 | .slds-vertical-tabs__nav { 2 | width: 4rem; 3 | } 4 | @media (min-width: 48rem) { 5 | .slds-vertical-tabs__nav { 6 | width: 12rem; 7 | } 8 | } 9 | .content, 10 | .contents { 11 | display: none; 12 | } 13 | .contents.show, 14 | .content.show { 15 | display: block; 16 | } 17 | 18 | .files { 19 | display: flex; 20 | justify-content: space-evenly; 21 | align-items: center; 22 | } 23 | .filename { 24 | flex: 1; 25 | } 26 | .filename.selected { 27 | background-color: #000; 28 | color: #fff; 29 | } 30 | 31 | .preview-content.selected { 32 | flex: 1; 33 | overflow: hidden; 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/my/preview/preview.html: -------------------------------------------------------------------------------- 1 | 127 | -------------------------------------------------------------------------------- /src/modules/my/preview/preview.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api } from 'lwc'; 8 | 9 | export default class Preview extends LightningElement { 10 | @api 11 | contents; 12 | 13 | selected = 'meta'; 14 | 15 | showHtml() { 16 | this.selected = 'html'; 17 | } 18 | showJs() { 19 | this.selected = 'js'; 20 | } 21 | showCss() { 22 | this.selected = 'css'; 23 | } 24 | showMeta() { 25 | this.selected = 'meta'; 26 | } 27 | showSvg() { 28 | this.selected = 'svg'; 29 | } 30 | showTest() { 31 | this.selected = 'test'; 32 | } 33 | 34 | get htmlContentClass() { 35 | return this.contentClass('html'); 36 | } 37 | get jsContentClass() { 38 | return this.contentClass('js'); 39 | } 40 | get cssContentClass() { 41 | return this.contentClass('css'); 42 | } 43 | get metaContentClass() { 44 | return this.contentClass('meta'); 45 | } 46 | get svgContentClass() { 47 | return this.contentClass('svg'); 48 | } 49 | get testContentClass() { 50 | return this.contentClass('test'); 51 | } 52 | 53 | contentClass(contentType) { 54 | return this.selected === contentType 55 | ? `preview-content selected ${contentType}` 56 | : contentType; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/my/previewContent/__tests__/previewContent.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { createElement } from 'lwc'; 9 | import MyPreviewContent from 'my/previewContent'; 10 | 11 | describe('my-preview-content', () => { 12 | afterEach(() => { 13 | // The jsdom instance is shared across test cases in a single file so reset the DOM 14 | while (document.body.firstChild) { 15 | document.body.removeChild(document.body.firstChild); 16 | } 17 | }); 18 | 19 | it('computes correct classes when selected', () => { 20 | // GIVEN 21 | const element = createElement('my-preview-content', { 22 | is: MyPreviewContent 23 | }); 24 | 25 | // WHEN 26 | element.type = 'html'; 27 | element.selected = 'html'; 28 | document.body.appendChild(element); 29 | 30 | // THEN 31 | const tagClassList = [ 32 | 'slds-vertical-tabs__content', 33 | 'preview-content-wrapper', 34 | 'slds-show' 35 | ]; 36 | const div = element.shadowRoot.querySelector('div'); 37 | tagClassList.forEach((cls) => { 38 | expect(div.classList.contains(cls)).toBe(true); 39 | }); 40 | }); 41 | 42 | it('computes correct classes when not selected', () => { 43 | // GIVEN 44 | const element = createElement('my-preview-content', { 45 | is: MyPreviewContent 46 | }); 47 | 48 | // WHEN 49 | element.type = 'html'; 50 | element.selected = 'js'; 51 | document.body.appendChild(element); 52 | 53 | // THEN 54 | const tagClassList = [ 55 | 'slds-vertical-tabs__content', 56 | 'preview-content-wrapper' 57 | ]; 58 | const div = element.shadowRoot.querySelector('div'); 59 | tagClassList.forEach((cls) => { 60 | expect(div.classList.contains(cls)).toBe(true); 61 | }); 62 | }); 63 | 64 | it('computes correct filename when prefix', () => { 65 | // GIVEN 66 | const element = createElement('my-preview-content', { 67 | is: MyPreviewContent 68 | }); 69 | 70 | // WHEN 71 | element.prefix = 'prefix'; 72 | element.filename = 'MyLWC'; 73 | element.extension = 'html'; 74 | document.body.appendChild(element); 75 | 76 | // THEN 77 | const div = element.shadowRoot.querySelector('div'); 78 | expect(div.id).toContain('prefixMyLWC.html'); 79 | }); 80 | 81 | it('computes correct filename when no prefix', () => { 82 | // GIVEN 83 | const element = createElement('my-preview-content', { 84 | is: MyPreviewContent 85 | }); 86 | 87 | // WHEN 88 | element.filename = 'MyLWC'; 89 | element.extension = 'html'; 90 | document.body.appendChild(element); 91 | 92 | // THEN 93 | const div = element.shadowRoot.querySelector('div'); 94 | expect(div.id).toContain('MyLWC.html'); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/modules/my/previewContent/previewContent.css: -------------------------------------------------------------------------------- 1 | .preview-content-wrapper { 2 | height: 100%; 3 | overflow: scroll; 4 | padding: 0; 5 | } 6 | .slds-text-longform { 7 | height: 100%; 8 | } 9 | .preview-source { 10 | height: 100%; 11 | margin: 0; 12 | padding: 1rem; 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/my/previewContent/previewContent.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/modules/my/previewContent/previewContent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api } from 'lwc'; 8 | 9 | export default class PreviewContent extends LightningElement { 10 | @api 11 | filename; 12 | 13 | @api 14 | extension; 15 | 16 | @api 17 | prefix; 18 | 19 | @api 20 | type; 21 | 22 | @api 23 | selected; 24 | 25 | @api 26 | content; 27 | 28 | get className() { 29 | const tagClass = 'slds-vertical-tabs__content preview-content-wrapper'; 30 | return this.selected === this.type 31 | ? `${tagClass} slds-show` 32 | : `${tagClass} slds-hide`; 33 | } 34 | 35 | get fileName() { 36 | return `${this.prefix ? this.prefix : ''}${this.filename}.${ 37 | this.extension 38 | }`; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/my/previewHeader/__tests__/previewHeader.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | import { createElement } from 'lwc'; 9 | import MyPreviewHeader from 'my/previewHeader'; 10 | 11 | describe('my-preview-header', () => { 12 | afterEach(() => { 13 | // The jsdom instance is shared across test cases in a single file so reset the DOM 14 | while (document.body.firstChild) { 15 | document.body.removeChild(document.body.firstChild); 16 | } 17 | }); 18 | 19 | it('computes correct classes when selected', () => { 20 | // GIVEN 21 | const element = createElement('my-preview-header', { 22 | is: MyPreviewHeader 23 | }); 24 | 25 | // WHEN 26 | element.type = 'html'; 27 | element.selected = 'html'; 28 | document.body.appendChild(element); 29 | 30 | // THEN 31 | const tagClassList = [ 32 | 'preview-header', 33 | 'slds-vertical-tabs__nav-item', 34 | 'slds-is-active' 35 | ]; 36 | const li = element.shadowRoot.querySelector('li'); 37 | tagClassList.forEach((cls) => { 38 | expect(li.classList.contains(cls)).toBe(true); 39 | }); 40 | }); 41 | 42 | it('computes correct classes when not selected', () => { 43 | // GIVEN 44 | const element = createElement('my-preview-header', { 45 | is: MyPreviewHeader 46 | }); 47 | 48 | // WHEN 49 | element.type = 'html'; 50 | element.selected = 'js'; 51 | document.body.appendChild(element); 52 | 53 | // THEN 54 | const tagClassList = ['preview-header', 'slds-vertical-tabs__nav-item']; 55 | const li = element.shadowRoot.querySelector('li'); 56 | tagClassList.forEach((cls) => { 57 | expect(li.classList.contains(cls)).toBe(true); 58 | }); 59 | }); 60 | 61 | it('computes correct filename when prefix', () => { 62 | // GIVEN 63 | const element = createElement('my-preview-header', { 64 | is: MyPreviewHeader 65 | }); 66 | 67 | // WHEN 68 | element.prefix = 'prefix'; 69 | element.filename = 'MyLWC'; 70 | element.extension = 'html'; 71 | document.body.appendChild(element); 72 | 73 | // THEN 74 | const li = element.shadowRoot.querySelector('li'); 75 | expect(li.title).toContain('prefixMyLWC.html'); 76 | }); 77 | 78 | it('computes correct filename when no prefix', () => { 79 | // GIVEN 80 | const element = createElement('my-preview-header', { 81 | is: MyPreviewHeader 82 | }); 83 | 84 | // WHEN 85 | element.filename = 'MyLWC'; 86 | element.extension = 'html'; 87 | document.body.appendChild(element); 88 | 89 | // THEN 90 | const li = element.shadowRoot.querySelector('li'); 91 | expect(li.title).toContain('MyLWC.html'); 92 | }); 93 | 94 | it('computes correct tab index when selected', () => { 95 | // GIVEN 96 | const element = createElement('my-preview-header', { 97 | is: MyPreviewHeader 98 | }); 99 | 100 | // WHEN 101 | element.type = 'html'; 102 | element.selected = 'html'; 103 | document.body.appendChild(element); 104 | 105 | // THEN 106 | const a = element.shadowRoot.querySelector('a'); 107 | expect(a.tabIndex).toBe(0); 108 | }); 109 | 110 | it('computes correct tab index when not selected', () => { 111 | // GIVEN 112 | const element = createElement('my-preview-header', { 113 | is: MyPreviewHeader 114 | }); 115 | 116 | // WHEN 117 | element.type = 'html'; 118 | element.selected = 'js'; 119 | document.body.appendChild(element); 120 | 121 | // THEN 122 | const a = element.shadowRoot.querySelector('a'); 123 | expect(a.tabIndex).toBe(-1); 124 | }); 125 | 126 | it('computes correct isSelected when selected', () => { 127 | // GIVEN 128 | const element = createElement('my-preview-header', { 129 | is: MyPreviewHeader 130 | }); 131 | 132 | // WHEN 133 | element.type = 'html'; 134 | element.selected = 'html'; 135 | document.body.appendChild(element); 136 | 137 | // THEN 138 | const a = element.shadowRoot.querySelector('a'); 139 | expect(a.ariaSelected).toBe('true'); 140 | }); 141 | 142 | it('computes correct isSelected when not selected', () => { 143 | // GIVEN 144 | const element = createElement('my-preview-header', { 145 | is: MyPreviewHeader 146 | }); 147 | 148 | // WHEN 149 | element.type = 'html'; 150 | element.selected = 'js'; 151 | document.body.appendChild(element); 152 | 153 | // THEN 154 | const a = element.shadowRoot.querySelector('a'); 155 | expect(a.ariaSelected).toBe('false'); 156 | }); 157 | 158 | it('fires click tab event when a clicked', () => { 159 | // GIVEN 160 | const element = createElement('my-preview-header', { 161 | is: MyPreviewHeader 162 | }); 163 | const eventListener = jest.fn(); 164 | element.addEventListener('clicktab', eventListener); 165 | document.body.appendChild(element); 166 | 167 | // WHEN 168 | const a = element.shadowRoot.querySelector('a'); 169 | a.click(); 170 | 171 | // THEN 172 | expect(eventListener).toHaveBeenCalled(); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /src/modules/my/previewHeader/previewHeader.css: -------------------------------------------------------------------------------- 1 | .preview-header.slds-text-heading--label, 2 | .preview-header.slds-text-heading_label { 3 | text-transform: none !important; 4 | } 5 | 6 | .preview-header-file-name { 7 | display: none; 8 | } 9 | .preview-header-file-type { 10 | display: block; 11 | } 12 | 13 | @media (min-width: 48rem) { 14 | .preview-header-file-name { 15 | display: block; 16 | } 17 | .preview-header-file-type { 18 | display: none; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/my/previewHeader/previewHeader.html: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/modules/my/previewHeader/previewHeader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api } from 'lwc'; 8 | 9 | export default class PreviewHeader extends LightningElement { 10 | @api 11 | filename; 12 | 13 | @api 14 | extension; 15 | 16 | @api 17 | prefix; 18 | 19 | @api 20 | type; 21 | 22 | @api 23 | selected; 24 | 25 | get className() { 26 | const tagClass = 'preview-header slds-vertical-tabs__nav-item'; 27 | return this.selected === this.type 28 | ? `${tagClass} slds-is-active` 29 | : tagClass; 30 | } 31 | 32 | get tabIndex() { 33 | return this.selected === this.type ? '0' : '-1'; 34 | } 35 | 36 | get isSelected() { 37 | return this.selected === this.type ? 'true' : 'false'; 38 | } 39 | 40 | get fileName() { 41 | return `${this.prefix ? this.prefix : ''}${this.filename}.${ 42 | this.extension 43 | }`; 44 | } 45 | 46 | onclickTab() { 47 | this.dispatchEvent( 48 | new CustomEvent('clicktab', { 49 | detail: this.extension 50 | }) 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/my/propertyCmsFilterItem/propertyCmsFilterItem.css: -------------------------------------------------------------------------------- 1 | .custom-filter-item { 2 | display: flex; 3 | } 4 | .custom-filter-item > input[type='text'] { 5 | flex: 1; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/my/propertyCmsFilterItem/propertyCmsFilterItem.html: -------------------------------------------------------------------------------- 1 | 55 | -------------------------------------------------------------------------------- /src/modules/my/propertyCmsFilterItem/propertyCmsFilterItem.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api } from 'lwc'; 8 | export default class PropertyCmsFilterItem extends LightningElement { 9 | @api 10 | filter; 11 | 12 | @api 13 | isCustom = false; 14 | 15 | isChecked = false; 16 | 17 | customFilterValue = ''; 18 | 19 | onChangeCheckbox = (e) => { 20 | this.isChecked = e.currentTarget.checked; 21 | this.dispatchEvent( 22 | new CustomEvent('changefilteritem', { 23 | detail: { 24 | filter: this.filter, 25 | isCustom: this.isCustom, 26 | isChecked: this.isChecked 27 | } 28 | }) 29 | ); 30 | }; 31 | 32 | onChangeInput = (e) => { 33 | this.customFilterValue = e.currentTarget.value; 34 | this.dispatchEvent( 35 | new CustomEvent('changefilteritem', { 36 | detail: { 37 | filter: { ...this.filter, value: this.customFilterValue }, 38 | isCustom: this.isCustom, 39 | isChecked: !!this.customFilterValue 40 | } 41 | }) 42 | ); 43 | }; 44 | 45 | deleteCustomFilter = () => { 46 | this.dispatchEvent( 47 | new CustomEvent('deletefilteritem', { 48 | detail: { 49 | filterId: this.filter.id 50 | } 51 | }) 52 | ); 53 | }; 54 | 55 | get filterId() { 56 | return `propFilter${this.filter.value}`; 57 | } 58 | 59 | get customFilterId() { 60 | return `propCustomFilter${this.filter.id}`; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/my/propertyDefinition/propertyDefinition.css: -------------------------------------------------------------------------------- 1 | .property-wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 0.75rem; 5 | border: 1px solid #dddbda; 6 | border-radius: 0.25rem; 7 | background-clip: padding-box; 8 | -webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); 9 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.1); 10 | margin-bottom: 0.75rem; 11 | } 12 | .alert-message { 13 | color: #f86363; 14 | font-style: italic; 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/my/propertyDefinition/propertyDefinition.html: -------------------------------------------------------------------------------- 1 | 513 | -------------------------------------------------------------------------------- /src/modules/my/propertyDefinition/propertyDefinition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api, track } from 'lwc'; 8 | import { sentenceCase, camelCase } from 'change-case'; 9 | 10 | export default class PropertyDefinition extends LightningElement { 11 | @api pid = ''; 12 | 13 | @api targets = []; 14 | 15 | @track 16 | property = { 17 | name: '', 18 | targets: [], 19 | selectedTargets: [], 20 | type: '', 21 | description: '', 22 | label: '', 23 | default: '', 24 | apexClassName: '', 25 | sObjectName: '', 26 | min: '', 27 | max: '', 28 | placeholder: '', 29 | required: false, 30 | flowInput: true, 31 | flowOutput: true, 32 | datasource: '', 33 | cmsFilters: [], 34 | screenResponsive: false 35 | }; 36 | 37 | filters = [ 38 | { id: 'cms_document', name: 'CMS Document', value: 'cms_document' }, 39 | { id: 'cms_image', name: 'CMS Image', value: 'cms_image' }, 40 | { id: 'cms_video', name: 'CMS Video', value: 'cms_video' }, 41 | { id: 'news', name: 'News', value: 'news' } 42 | ]; 43 | 44 | @track 45 | customFilters = [{ id: 1, value: '' }]; 46 | nextCustomFilterId = 2; 47 | 48 | nonPropertyTargets = [ 49 | 'lightning__Tab', 50 | 'lightningSnapin__ChatMessage', 51 | 'lightningCommunity__Page', 52 | 'lightningSnapin__Minimized', 53 | 'lightningSnapin__PreChat', 54 | 'lightningSnapin__ChatHeader' 55 | ]; 56 | 57 | get enabledTargets() { 58 | const enabled = Object.values(this.targets) 59 | .filter((t) => t.enabled) 60 | .filter((t) => !this.nonPropertyTargets.includes(t.value)); 61 | return enabled; 62 | } 63 | 64 | get isCommunityDefaultOnly() { 65 | return ( 66 | this.property.selectedTargets.length === 1 && 67 | this.property.selectedTargets[0] === 'lightningCommunity__Default' 68 | ); 69 | } 70 | 71 | get isFlowOnly() { 72 | return ( 73 | this.property.selectedTargets.length === 1 && 74 | this.property.selectedTargets[0] === 'lightning__FlowScreen' 75 | ); 76 | } 77 | 78 | get supportsFlow() { 79 | return !!this.property.selectedTargets.find( 80 | (t) => t === 'lightning__FlowScreen' 81 | ); 82 | } 83 | 84 | get isEmailOnly() { 85 | return ( 86 | this.property.selectedTargets.length === 1 && 87 | this.property.selectedTargets[0] === 'lightningStatic__Email' 88 | ); 89 | } 90 | 91 | get isAnalyticsOnly() { 92 | return ( 93 | this.property.selectedTargets.length === 1 && 94 | this.property.selectedTargets[0] === 'analytics__Dashboard' 95 | ); 96 | } 97 | 98 | get supportsScreenResponsive() { 99 | return this.isCommunityDefaultOnly && this.isInteger; 100 | } 101 | 102 | get isApexClassType() { 103 | return this.property.type === 'apex'; 104 | } 105 | 106 | get isSObjectType() { 107 | return this.property.type === 'sobject'; 108 | } 109 | 110 | get isDateType() { 111 | return this.property.type === 'Date'; 112 | } 113 | get isDateTimeType() { 114 | return this.property.type === 'DateTime'; 115 | } 116 | 117 | get isBoolean() { 118 | return this.property.type === 'Boolean'; 119 | } 120 | 121 | get isAlignment() { 122 | return ( 123 | this.property.type === 'HorizontalAlignment' || 124 | this.property.type === 'VerticalAlignment' 125 | ); 126 | } 127 | 128 | get isHorizontalAlignment() { 129 | return this.property.type === 'HorizontalAlignment'; 130 | } 131 | 132 | get isInteger() { 133 | return this.property.type === 'Integer'; 134 | } 135 | 136 | get isString() { 137 | return this.property.type === 'String'; 138 | } 139 | get propertyNameId() { 140 | return `propertyName_${this.pid}`; 141 | } 142 | get labelId() { 143 | return `label_${this.pid}`; 144 | } 145 | get descId() { 146 | return `desc_${this.pid}`; 147 | } 148 | get defaultId() { 149 | return `default_${this.pid}`; 150 | } 151 | 152 | get typeId() { 153 | return `type_${this.pid}`; 154 | } 155 | 156 | get apexClassNameId() { 157 | return `apexClassName_${this.pid}`; 158 | } 159 | 160 | get sObjectNameId() { 161 | return `sObjectName_${this.pid}`; 162 | } 163 | 164 | get minId() { 165 | return `min_${this.pid}`; 166 | } 167 | get maxId() { 168 | return `max_${this.pid}`; 169 | } 170 | get placeholderId() { 171 | return `placeholder_${this.pid}`; 172 | } 173 | get requiredId() { 174 | return `required_${this.pid}`; 175 | } 176 | get flowInputId() { 177 | return `flowInput_${this.pid}`; 178 | } 179 | get flowOutputId() { 180 | return `flowOutput_${this.pid}`; 181 | } 182 | get datasourceId() { 183 | return `datasource_${this.pid}`; 184 | } 185 | get screenResponsiveId() { 186 | return `screenResponsive_${this.pid}`; 187 | } 188 | 189 | get isDefaultEnabled() { 190 | // TODO: consider Dimension and Measure type 191 | return this.property.type !== 'apex' && this.property.type !== 'sobject'; 192 | } 193 | 194 | get isContentReferenceEnabled() { 195 | return this.property.type === 'ContentReference'; 196 | } 197 | 198 | get defaultType() { 199 | switch (this.property.type) { 200 | case 'Integer': 201 | return 'number'; 202 | case 'Date': 203 | return 'date'; 204 | case 'DateTime': 205 | return 'datetime-local'; 206 | default: 207 | return 'text'; 208 | } 209 | } 210 | 211 | get hasCustomCmsContentTypeFilter() { 212 | return this.customFilters.find((f) => !!f.value); 213 | } 214 | 215 | onChangeInput = (e) => { 216 | const key = e.target.attributes.name.value; 217 | const formattedValue = this.formatInputValue(key, e.target.value); 218 | this.property[key] = formattedValue; 219 | 220 | if (key === 'name' && !this.property.label) { 221 | this.property.label = sentenceCase(formattedValue); 222 | } 223 | this.updateProperty(); 224 | }; 225 | 226 | onChangeSelect = (e) => { 227 | const key = e.target.attributes.name.value; 228 | this.property[key] = e.target.value; 229 | // reset default, min, and max values on type change 230 | if (key === 'type') { 231 | this.property.min = ''; 232 | this.property.max = ''; 233 | this.property.default = ''; 234 | } 235 | this.updateProperty(); 236 | }; 237 | 238 | onChangeCheckbox = (e) => { 239 | const key = e.target.attributes.name.value; 240 | this.property[key] = e.target.checked; 241 | 242 | // fix for "You can’t make an output only property required."" 243 | if ( 244 | this.supportsFlow && 245 | key === 'required' && 246 | e.target.checked && 247 | !this.property.flowInput 248 | ) { 249 | this.property.flowInput = true; 250 | } 251 | this.updateProperty(); 252 | }; 253 | 254 | onChangeTargetCheckbox = (e) => { 255 | const { target, isChecked } = e.detail; 256 | if (isChecked) { 257 | this.property.selectedTargets.push(target.value); 258 | } else { 259 | const indexOfTarget = this.property.selectedTargets.indexOf(target.value); 260 | if (indexOfTarget > -1) { 261 | this.property.selectedTargets.splice(indexOfTarget, 1); 262 | } 263 | } 264 | this.updateProperty(); 265 | }; 266 | 267 | onChangeFilterCheckbox = (e) => { 268 | const { filter, isCustom, isChecked } = e.detail; 269 | 270 | if (isCustom) { 271 | const cfId = this.customFilters.findIndex((f) => f.id === filter.id); 272 | if (cfId > -1) { 273 | this.customFilters[cfId] = filter; 274 | } 275 | } 276 | 277 | const fId = this.property.cmsFilters.findIndex((f) => f.id === filter.id); 278 | 279 | if (isChecked) { 280 | if (isCustom && fId > -1) { 281 | this.property.cmsFilters[fId] = filter; 282 | } else { 283 | this.property.cmsFilters.push(filter); 284 | } 285 | } else if (fId > -1) { 286 | this.property.cmsFilters.splice(fId, 1); 287 | } 288 | this.updateProperty(); 289 | }; 290 | 291 | formatInputValue = (key, value) => { 292 | if (key === 'name') { 293 | // restrict starting from digits 294 | return camelCase(value.replace(/^\d+/, '')); 295 | } 296 | return value; 297 | }; 298 | 299 | updateProperty = () => { 300 | this.dispatchEvent( 301 | new CustomEvent('changepropdef', { 302 | detail: { 303 | ...this.property, 304 | id: this.pid 305 | } 306 | }) 307 | ); 308 | }; 309 | 310 | deletePropertyRow = () => { 311 | this.dispatchEvent( 312 | new CustomEvent('deletepropdef', { 313 | detail: this.pid 314 | }) 315 | ); 316 | }; 317 | 318 | addCustomFilterRow = () => { 319 | this.customFilters.push({ id: this.nextCustomFilterId, value: '' }); 320 | this.nextCustomFilterId++; 321 | }; 322 | 323 | onDeleteCustomFilterItem = (e) => { 324 | const cfIndex = this.customFilters.findIndex( 325 | (f) => f.id === e.detail.filterId 326 | ); 327 | if (cfIndex > -1) { 328 | this.customFilters.splice(cfIndex, 1); 329 | } 330 | const fIndex = this.property.cmsFilters.findIndex( 331 | (f) => f.id === e.detail.filterId 332 | ); 333 | if (fIndex > -1) { 334 | this.property.cmsFilters.splice(fIndex, 1); 335 | } 336 | this.updateProperty(); 337 | }; 338 | } 339 | -------------------------------------------------------------------------------- /src/modules/my/propertyTargetItem/propertyTargetItem.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/modules/my/propertyTargetItem/propertyTargetItem.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api } from 'lwc'; 8 | 9 | export default class PropertyTargetItem extends LightningElement { 10 | @api 11 | target; 12 | 13 | isChecked = false; 14 | 15 | onChangeTargetCheckbox = (e) => { 16 | this.isChecked = e.currentTarget.checked; 17 | this.dispatchEvent( 18 | new CustomEvent('changepropitem', { 19 | detail: { 20 | target: this.target, 21 | isChecked: this.isChecked 22 | } 23 | }) 24 | ); 25 | }; 26 | 27 | get targetId() { 28 | return `propTarget${this.target.value}`; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/my/sobjectDefinition/sobjectDefinition.css: -------------------------------------------------------------------------------- 1 | .sobject-wrapper { 2 | display: flex; 3 | } 4 | .sobject-input { 5 | flex: 1; 6 | } 7 | .sobject-delete-button { 8 | margin-left: 0.5rem; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/my/sobjectDefinition/sobjectDefinition.html: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /src/modules/my/sobjectDefinition/sobjectDefinition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api } from 'lwc'; 8 | 9 | export default class SobjectDefinition extends LightningElement { 10 | @api 11 | oid = ''; 12 | 13 | name = ''; 14 | 15 | onChangeInput = (e) => { 16 | this.name = e.target.value; 17 | this.dispatchEvent( 18 | new CustomEvent('changesobj', { 19 | detail: { 20 | id: this.oid, 21 | name: this.name 22 | } 23 | }) 24 | ); 25 | }; 26 | 27 | deleteRow = () => { 28 | this.dispatchEvent( 29 | new CustomEvent('deletesobj', { 30 | detail: this.oid 31 | }) 32 | ); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/my/svgUploader/svgUploader.css: -------------------------------------------------------------------------------- 1 | .input, 2 | .output { 3 | display: none; 4 | padding: 0.25rem; 5 | } 6 | 7 | .input.show, 8 | .output.show { 9 | display: block; 10 | } 11 | .remove-button { 12 | vertical-align: top; 13 | margin-right: 0.25rem; 14 | display: inline-block; 15 | } 16 | 17 | .svg-filename { 18 | font-style: italic; 19 | color: #444; 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/my/svgUploader/svgUploader.html: -------------------------------------------------------------------------------- 1 | 67 | -------------------------------------------------------------------------------- /src/modules/my/svgUploader/svgUploader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { LightningElement, api } from 'lwc'; 8 | 9 | export default class SvgUploader extends LightningElement { 10 | static MAX_SIZE = 200000; 11 | 12 | @api 13 | fileName; 14 | 15 | @api 16 | fileContent; 17 | 18 | discardSvg = () => { 19 | this.dispatchEvent( 20 | new CustomEvent('uploadsvg', { 21 | detail: { 22 | fileName: '', 23 | fileContent: '' 24 | } 25 | }) 26 | ); 27 | }; 28 | 29 | get showOutputClass() { 30 | return this.fileName ? 'output show' : 'output'; 31 | } 32 | get showInputClass() { 33 | return this.fileName ? 'input' : 'input show'; 34 | } 35 | 36 | loadSvgFile = (e) => { 37 | const file = e.currentTarget.files[0]; 38 | if (file && file.size < SvgUploader.MAX_SIZE) { 39 | const reader = new FileReader(); 40 | reader.readAsText(file, 'UTF-8'); 41 | reader.onload = (re) => { 42 | if (re.target.result) { 43 | this.dispatchEvent( 44 | new CustomEvent('uploadsvg', { 45 | detail: { 46 | fileName: file.name, 47 | fileContent: re.target.result 48 | } 49 | }) 50 | ); 51 | } 52 | }; 53 | reader.onerror = (re) => { 54 | console.error(re); 55 | }; 56 | e.currentTarget.type = 'text'; 57 | e.currentTarget.type = 'file'; 58 | } 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/modules/my/targetDefinition/targetDefinition.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forcedotcom/lwc-builder-ui/01f5a44a84e68aa80a4929f5ab37912aba1929c1/src/modules/my/targetDefinition/targetDefinition.css -------------------------------------------------------------------------------- /src/modules/my/targetDefinition/targetDefinition.html: -------------------------------------------------------------------------------- 1 | 124 | -------------------------------------------------------------------------------- /src/modules/my/targetDefinition/targetDefinition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * Licensed under the BSD 3-Clause license. 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | import { api, LightningElement } from 'lwc'; 8 | export default class TargetDefinition extends LightningElement { 9 | @api 10 | target; 11 | 12 | enabled; 13 | 14 | connectedCallback() { 15 | this.enableSmall = this.isSmallFormFactorSupported; 16 | this.enableLarge = this.isFormFactorSupported; 17 | this.enableHeadless = false; 18 | this.enableHasStep = false; 19 | this.enabled = false; 20 | } 21 | 22 | onChangeLargeCheckbox = (e) => { 23 | this.enableLarge = e.target.checked; 24 | this.onChange(); 25 | }; 26 | onChangeSmallCheckbox = (e) => { 27 | this.enableSmall = e.target.checked; 28 | this.onChange(); 29 | }; 30 | 31 | onChangeActionTypeCheckbox = (e) => { 32 | this.enableHeadless = e.target.checked; 33 | this.onChange(); 34 | }; 35 | 36 | onChangeHasStepCheckbox = (e) => { 37 | this.enableHasStep = e.target.checked; 38 | this.onChange(); 39 | }; 40 | 41 | onChangeTargetCheckbox = (e) => { 42 | this.enabled = e.target.checked; 43 | this.onChange(); 44 | }; 45 | 46 | get disabledCheck() { 47 | return !this.enabled; 48 | } 49 | 50 | get largeFormId() { 51 | return `${this.target.value}-form-large`; 52 | } 53 | 54 | get smallFormId() { 55 | return `${this.target.value}-form-small`; 56 | } 57 | 58 | onChange = () => { 59 | this.dispatchEvent( 60 | new CustomEvent('changetarget', { 61 | detail: { 62 | target: this.target, 63 | enabled: this.enabled, 64 | small: this.enableSmall, 65 | large: this.enableLarge, 66 | headlessAction: this.enableHeadless, 67 | hasStep: this.enableHasStep 68 | } 69 | }) 70 | ); 71 | }; 72 | 73 | /* Form Factor */ 74 | get isFormFactorSupported() { 75 | return ( 76 | this.target.value === 'lightning__AppPage' || 77 | this.target.value === 'lightning__RecordPage' || 78 | this.target.value === 'lightning__HomePage' 79 | ); 80 | } 81 | get isSmallFormFactorSupported() { 82 | return ( 83 | this.target.value === 'lightning__AppPage' || 84 | this.target.value === 'lightning__RecordPage' 85 | ); 86 | } 87 | 88 | /* Quick Action */ 89 | get isActionTypeSupported() { 90 | return this.target.value === 'lightning__RecordAction'; 91 | } 92 | 93 | get actionTypeFormId() { 94 | return `${this.target.value}-action-type`; 95 | } 96 | 97 | /* CRM Analytics dashboard */ 98 | get isHasStepSupported() { 99 | return this.target.value === 'analytics__Dashboard'; 100 | } 101 | 102 | get hasStepFormId() { 103 | return `${this.target.value}-has-step`; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forcedotcom/lwc-builder-ui/01f5a44a84e68aa80a4929f5ab37912aba1929c1/src/resources/favicon.ico -------------------------------------------------------------------------------- /src/resources/lwc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forcedotcom/lwc-builder-ui/01f5a44a84e68aa80a4929f5ab37912aba1929c1/src/resources/lwc.png --------------------------------------------------------------------------------