Merge submodule contents for src/Umbraco.Web.UI.Client/main
This commit is contained in:
29
src/Umbraco.Web.UI.Client/.editorconfig
Normal file
29
src/Umbraco.Web.UI.Client/.editorconfig
Normal file
@@ -0,0 +1,29 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# Change these settings to your own preference
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
|
||||
# We recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.{html,js,md}]
|
||||
block_comment_start = /**
|
||||
block_comment = *
|
||||
block_comment_end = */
|
||||
6
src/Umbraco.Web.UI.Client/.env
Normal file
6
src/Umbraco.Web.UI.Client/.env
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copy this to .env.local and change what you want to test.
|
||||
VITE_UMBRACO_USE_MSW=on # on = turns on MSW, off = disables all mock handlers
|
||||
VITE_UMBRACO_API_URL=https://localhost:44339
|
||||
VITE_UMBRACO_INSTALL_STATUS=running # running or must-install or must-upgrade
|
||||
VITE_MSW_QUIET=off # on = turns off MSW console logs, off = turns on MSW console logs
|
||||
VITE_UMBRACO_EXTENSION_MOCKS=off # on = turns on extension mocks, off = turns off extension mocks
|
||||
3
src/Umbraco.Web.UI.Client/.env.e2e
Normal file
3
src/Umbraco.Web.UI.Client/.env.e2e
Normal file
@@ -0,0 +1,3 @@
|
||||
# Copy this to .env.local and change what you want to test.
|
||||
VITE_UMBRACO_USE_MSW=off # Playwright handles the mocking itself (using msw but it starts it up manually)
|
||||
VITE_UMBRACO_API_URL=
|
||||
2
src/Umbraco.Web.UI.Client/.env.production
Normal file
2
src/Umbraco.Web.UI.Client/.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_UMBRACO_USE_MSW=off
|
||||
VITE_UMBRACO_API_URL=
|
||||
2
src/Umbraco.Web.UI.Client/.env.staging
Normal file
2
src/Umbraco.Web.UI.Client/.env.staging
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_UMBRACO_INSTALL_STATUS=running # running or must-install or must-upgrade
|
||||
VITE_UMBRACO_USE_MSW=on
|
||||
239
src/Umbraco.Web.UI.Client/.github/CONTRIBUTING.md
vendored
Normal file
239
src/Umbraco.Web.UI.Client/.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
## Thoughts, links, and questions
|
||||
|
||||
In the high probability that you are porting something from angular JS then here are a few helpful tips for using Lit:
|
||||
|
||||
Here is the LIT documentation and playground: [https://lit.dev](https://lit.dev)
|
||||
|
||||
### How best to find what needs converting from the old backoffice?
|
||||
|
||||
1. Navigate to [https://github.com/umbraco/Umbraco-CMS](https://github.com/umbraco/Umbraco-CMS)
|
||||
2. Make sure you are on the `v14/dev` branch
|
||||
|
||||
### What is the process of contribution?
|
||||
|
||||
- Read the [README](README.md) to learn how to get the project up and running
|
||||
- Find an issue marked as [community/up-for-grabs](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs) - note that some are also marked [good first issue](https://github.com/umbraco/Umbraco.CMS.Backoffice/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) which indicates they are simple to get started on
|
||||
- Umbraco HQ owns the Management API on the backend, so features can be worked on in the frontend only when there is an API, or otherwise if no API is required
|
||||
- A contribution should be made in a fork of the repository
|
||||
- Once a contribution is ready, a pull request should be made to this repository and HQ will assign a reviewer
|
||||
- A pull request should always indicate what part of a feature it tries to solve, i.e. does it close the targeted issue (if any) or does the developer expect Umbraco HQ to take over
|
||||
|
||||
## Contributing in general terms
|
||||
|
||||
A lot of the UI has already been migrated to the new backoffice. Generally speaking, one would find a feature on the projects board, locate the UI in the old backoffice (v11 is fine), convert it to Lit components using the UI library, put the business logic into a store/service, write tests, and make a pull request.
|
||||
|
||||
We are also very keen to receive contributions towards **documentation, unit testing, package development, accessibility, and just general testing of the UI.**
|
||||
|
||||
## The Management API
|
||||
|
||||
The management API is the colloquial term used to describe the new backoffice API. It is built as a .NET Web API, has a Swagger endpoint (/umbraco/swagger), and outputs an OpenAPI v3 schema, that the frontend consumes.
|
||||
|
||||
The frontend has an API formatter that takes the OpenAPI schema file and converts it into a set of TypeScript classes and interfaces.
|
||||
|
||||
### Caveats
|
||||
|
||||
1. The backoffice can be run and tested against a real Umbraco instance by cloning down the `v14/dev` branch, but there are no guarantees about how well it works yet.
|
||||
|
||||
**Current schema for API:**
|
||||
|
||||
[https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v13/dev/src/Umbraco.Cms.Api.Management/OpenApi.json](https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v14/dev/src/Umbraco.Cms.Api.Management/OpenApi.json)
|
||||
|
||||
**How to convert it:**
|
||||
|
||||
- Run `npm run generate:api`
|
||||
|
||||
## A contribution example
|
||||
|
||||
### Example: Published Cache Status Dashboard
|
||||
|
||||

|
||||
|
||||
### Boilerplate (example using Lit)
|
||||
|
||||
Links for Lit examples and documentation:
|
||||
|
||||
- [https://lit.dev](https://lit.dev)
|
||||
- [https://lit.dev/docs/](https://lit.dev/docs/)
|
||||
- [https://lit.dev/playground/](https://lit.dev/playground/)
|
||||
|
||||
### Functionality
|
||||
|
||||
**HTML**
|
||||
|
||||
The simplest approach is to copy over the HTML from the old backoffice into a new Lit element (check existing elements in the repository, e.g. if you are working with a dashboard, then check other dashboards, etc.). Once the HTML is inside the `render` method, it is often enough to simply replace `<umb-***>` elements with `<uui-***>` and replace a few of the attributes. In general, we try to build as much UI with Umbraco UI Library as possible.
|
||||
|
||||
**Controller**
|
||||
|
||||
The old AngularJS controllers will have to be converted into modern TypeScript and will have to use our new services and stores. We try to abstract as much away as possible, and mostly you will have to make API calls and let the rest of the system handle things like error handling and so on. In the case of this dashboard, we only have a few GET and POST requests. Looking at the new Management API, we find the PublishedCacheService, which is the new API controller to serve data to the dashboard.
|
||||
|
||||
To make the first button work, which simply just requests a new status from the server, we must make a call to `PublishedCacheService.getPublishedCacheStatus()`. An additional thing here is to wrap that in a friendly function called `tryExecuteAndNotify`, which is something we make available to developers to automatically handle the responses coming from the server and additionally use the Notifications to notify of any errors:
|
||||
|
||||
```typescript
|
||||
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
|
||||
import { PublishedCacheService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
private _getStatus() {
|
||||
const { data: status } = await tryExecuteAndNotify(this, PublishedCacheService.getPublishedCacheStatus());
|
||||
|
||||
if (status) {
|
||||
// we now have the status
|
||||
console.log(status);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State (buttons, etc)
|
||||
|
||||
It is a good idea to make buttons indicate a loading state when awaiting an API call. All `<uui-button>` support the `.state` property, which you can set around API calls:
|
||||
|
||||
```typescript
|
||||
@state()
|
||||
private _buttonState: UUIButtonState = undefined;
|
||||
|
||||
private _getStatus() {
|
||||
this._buttonState = 'waiting';
|
||||
|
||||
[...await...]
|
||||
|
||||
this._buttonState = 'success';
|
||||
}
|
||||
```
|
||||
|
||||
## Making the dashboard visible
|
||||
|
||||
### Add to internal manifests
|
||||
|
||||
All items are declared in a `manifests.ts` file, which is located in each section directory.
|
||||
|
||||
To declare the Published Cache Status Dashboard as a new manifest, we need to add the section as a new json object that would look like this:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.PublishedStatus',
|
||||
name: 'Published Status Dashboard',
|
||||
elementName: 'umb-dashboard-published-status',
|
||||
element: () => import('./published-status/dashboard-published-status.element.js'),
|
||||
weight: 200,
|
||||
meta: {
|
||||
label: 'Published Status',
|
||||
pathname: 'published-status',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.SectionAlias',
|
||||
match: 'Umb.Section.Settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
Let’s go through each of these properties…
|
||||
|
||||
- Type: can be one of the following:
|
||||
|
||||
- section - examples include: `Content`, `Media`
|
||||
- dashboard - a view within a section. Examples include: the welcome dashboard
|
||||
- propertyEditorUi
|
||||
- editorView
|
||||
- propertyAction
|
||||
- tree
|
||||
- editor
|
||||
- treeItemAction
|
||||
|
||||
- Alias: is the unique key used to identify this item.
|
||||
- Name: is the human-readable name for this item.
|
||||
|
||||
- ElementName: this is the customElementName declared on the element at the top of the file i.e
|
||||
|
||||
```typescript
|
||||
@customElement('umb-dashboard-published-status')
|
||||
```
|
||||
|
||||
- Js: references a function call to import the file that the element is declared within
|
||||
|
||||
- Weight: allows us to specify the order in which the dashboard will be displayed within the tabs bar
|
||||
|
||||
- Meta: allows us to reference additional data - in our case, we can specify the label that is shown in the tabs bar and the pathname that will be displayed in the URL
|
||||
|
||||
- Conditions: allows us to specify the conditions that must be met for the dashboard to be displayed. In our case, we are specifying that the dashboard will only be displayed within the Settings section
|
||||
|
||||
## API mock handlers
|
||||
|
||||
Running the app with `npm run dev`, you will quickly notice the API requests turn into 404 errors. To hit the API, we need to add a mock handler to define the endpoints that our dashboard will call. In the case of the Published Cache Status section, we have several calls to work through. Let’s start by looking at the call to retrieve the current status of the cache:
|
||||
|
||||

|
||||
|
||||
From the existing functionality, we can see that this is a string message that is received as part of a `GET` request from the server.
|
||||
|
||||
So to define this, we must first add a handler for the Published Status called `published-status.handlers.ts` within the mocks/domains folder. In this file we will have code that looks like the following:
|
||||
|
||||
```typescript
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export const handlers = [
|
||||
rest.get(umbracoPath('/published-cache/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<string>(
|
||||
'Database cache is ok. ContentStore contains 1 item and has 1 generation and 0 snapshot. MediaStore contains 5 items and has 1 generation and 0 snapshot.',
|
||||
),
|
||||
);
|
||||
}),
|
||||
];
|
||||
```
|
||||
|
||||
This is defining the `GET` path that we will call through the resource: `/published-cache/status`
|
||||
|
||||
It returns a `200 OK` response and a string value with the current “status” of the published cache for us to use within the element
|
||||
|
||||
An example `POST` is similar. Let’s take the “Refresh status” button as an example:
|
||||
|
||||

|
||||
|
||||
From our existing functionality, we can see that this makes a `POST` call to the server to prompt a reload of the published cache. So we would add a new endpoint to the mock handler that would look like:
|
||||
|
||||
```typescript
|
||||
rest.post(umbracoPath('/published-cache/reload'), async (_req, res, ctx) => {
|
||||
return res(
|
||||
// Simulate a 1 second delay for the benefit of the UI
|
||||
ctx.delay(1000)
|
||||
// Respond with a 201 status code
|
||||
ctx.status(201)
|
||||
);
|
||||
})
|
||||
```
|
||||
|
||||
Which is defining a new `POST` endpoint that we can add to the core API fetcher using the path `/published-cache/reload`.
|
||||
|
||||
This call returns a simple `OK` status code and no other object.
|
||||
|
||||
## Storybook stories
|
||||
|
||||
We try to make good Storybook stories for new components, which is a nice way to work with a component in an isolated state. Imagine you are working with a dialog on page 3 and have to navigate back to that every time you make a change - this is now eliminated with Storybook as you can just make a story that displays that step. Storybook can only show one component at a time, so it also helps us to isolate view logic into more and smaller components, which in turn are more testable.
|
||||
|
||||
In-depth: [https://storybook.js.org/docs/web-components/get-started/introduction](https://storybook.js.org/docs/web-components/get-started/introduction)
|
||||
|
||||
Reference: [https://ambitious-stone-0033b3603.1.azurestaticapps.net/](https://ambitious-stone-0033b3603.1.azurestaticapps.net/)
|
||||
|
||||
- Locally: `npm run storybook`
|
||||
|
||||
For Umbraco UI stories, please navigate to [https://uui.umbraco.com/](https://uui.umbraco.com/)
|
||||
|
||||
## Testing
|
||||
|
||||
There are two testing tools on the backoffice: unit testing and end-to-end testing.
|
||||
|
||||
### Unit testing
|
||||
|
||||
We are using a tool called Web Test Runner which spins up a bunch of browsers using Playwright with the well-known jasmine/chai syntax. It is expected that any new component/element has a test file named “<component>.test.ts”. It will automatically be picked up and there are a set of standard tests we apply to all components, which checks that they are registered correctly and they pass accessibility testing through Axe.
|
||||
|
||||
Working with playwright: [https://playwright.dev/docs/intro](https://playwright.dev/docs/intro)
|
||||
|
||||
## Putting it all together
|
||||
|
||||
When we are finished with the dashboard we will hopefully have something akin to this [real-world example of the actual dashboard that was migrated](https://github.com/umbraco/Umbraco.CMS.Backoffice/tree/main/src/backoffice/settings/dashboards/published-status).
|
||||
32
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/01_bug_report.md
vendored
Normal file
32
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/01_bug_report.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]: "
|
||||
labels: type/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/02_feature_request.md
vendored
Normal file
20
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/02_feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: "✨ Feature request"
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: type/feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
8
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
src/Umbraco.Web.UI.Client/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💡 Features and ideas
|
||||
url: https://github.com/umbraco/Umbraco.CMS.Backoffice/discussions/new?category=ideas
|
||||
about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans.
|
||||
- name: ❓ Support Question
|
||||
url: https://our.umbraco.com
|
||||
about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the discussions board or Discord.
|
||||
21
src/Umbraco.Web.UI.Client/.github/LICENSE
vendored
Normal file
21
src/Umbraco.Web.UI.Client/.github/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Umbraco HQ
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
76
src/Umbraco.Web.UI.Client/.github/README.md
vendored
Normal file
76
src/Umbraco.Web.UI.Client/.github/README.md
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# Umbraco.CMS.Backoffice (Bellissima)
|
||||
|
||||
This is the working repository of the upcoming new Backoffice to Umbraco CMS.
|
||||
|
||||
[](https://github.com/umbraco/Umbraco.CMS.Backoffice/actions/workflows/build_test.yml)
|
||||
[](https://github.com/umbraco/Umbraco.CMS.Backoffice/actions/workflows/azure-static-web-apps-ambitious-stone-0033b3603.yml)
|
||||
[](https://sonarcloud.io/summary/new_code?id=umbraco_Umbraco.CMS.Backoffice)
|
||||
|
||||
## Installation instructions
|
||||
|
||||
1. Run `npm install`
|
||||
2. Run `npm run dev` to launch Vite in dev mode
|
||||
|
||||
### Environment variables
|
||||
|
||||
The development environment supports `.env` files, so in order to set your own make a copy
|
||||
of `.env` and name it `.env.local` and set the variables you need.
|
||||
|
||||
As an example to show the installer instead of the login screen, set the following
|
||||
in the `.env.local` file to indicate that Umbraco has not been installed:
|
||||
|
||||
```bash
|
||||
VITE_UMBRACO_INSTALL_STATUS=must-install
|
||||
```
|
||||
|
||||
## Environments
|
||||
|
||||
### Development
|
||||
|
||||
The development environment is the default environment and is used when running `npm run dev`. All API calls are mocked and the Umbraco backoffice is served from the `src` folder.
|
||||
|
||||
### Run against a local Umbraco instance
|
||||
|
||||
If you have a local Umbraco instance running, you can use the development environment to run against it by overriding the API URL and bypassing the mock-service-worker in the frontend client.
|
||||
|
||||
Create a `.env.local` file and set the following variables:
|
||||
|
||||
```bash
|
||||
VITE_UMBRACO_API_URL=https://localhost:44339 # This will be the URL to your Umbraco instance
|
||||
VITE_UMBRACO_USE_MSW=off # Indicate that you want all API calls to bypass MSW (mock-service-worker)
|
||||
```
|
||||
|
||||
Open this file in an editor: `src/Umbraco.Web.UI/appsettings.Development.json` and add this to the `Umbraco:CMS:Security` section to override the backoffice host:
|
||||
|
||||
```json
|
||||
"Umbraco": {
|
||||
"CMS": {
|
||||
"Security":{
|
||||
"BackOfficeHost": "http://localhost:5173",
|
||||
"AuthorizeCallbackPathName": "/oauth_complete",
|
||||
"AuthorizeCallbackLogoutPathName": "/logout",
|
||||
"AuthorizeCallbackErrorPathName": "/error",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Now start the vite server: `npm run dev:server` in your backoffice folder and open the http://localhost:5173 URL in your browser.
|
||||
|
||||
### Storybook
|
||||
|
||||
Storybook is also being built and deployed automatically on the Main branch, including a preview URL on each pull request. See it in action on this [Azure Static Web App](https://ambitious-stone-0033b3603.1.azurestaticapps.net/).
|
||||
|
||||
You can test the Storybook locally by running `npm run storybook`. This will start the Storybook server and open a browser window with the Storybook UI.
|
||||
|
||||
Storybook is an excellent tool to test out UI components in isolation and to document them. It is also a great way to test the responsiveness and accessibility of the components.
|
||||
|
||||
## Contributing
|
||||
|
||||
We accept contributions to this project. However be aware that we are mainly working on a private backlog, so not everything will be immediately obvious. If you want to get started on contributing, please read the [contributing guidelines](./CONTRIBUTING.md).
|
||||
|
||||
A list of issues can be found on the [Umbraco-CMS Issue Tracker](https://github.com/umbraco/Umbraco-CMS/issues). Many of them are marked as `community/up-for-grabs` which means they are up for grabs for anyone to work on.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found on [Umbraco Docs](https://docs.umbraco.com/umbraco-cms).
|
||||
31
src/Umbraco.Web.UI.Client/.github/RELEASE_INSTRUCTION.md
vendored
Normal file
31
src/Umbraco.Web.UI.Client/.github/RELEASE_INSTRUCTION.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Bellissima release instructions
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
> _See internal documentation on the build/release workflow._
|
||||
|
||||
|
||||
## GitHub Release Notes
|
||||
|
||||
To generate release notes on GitHub.
|
||||
|
||||
- Go to the [**Releases** area](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases)
|
||||
- Press the [**"Draft a new release"** button](https://github.com/umbraco/Umbraco.CMS.Backoffice/releases/new)
|
||||
- In the combobox for "Choose a tag", expand then select or enter the next version number, e.g. `v14.2.0`
|
||||
- If the tag does not already exist, an option labelled "Create new tag: v14.2.0 on publish" will appear, select that option
|
||||
- In the combobox for "Target: main", expand then select the release branch for the next version, e.g. `release/14.2`
|
||||
- In the combobox for "Previous tag: auto":
|
||||
- If the next release is an RC, then you can leave as `auto`
|
||||
- Otherwise, select the previous stable version, e.g. `v14.1.1`
|
||||
- Press the **"Generate release notes"** button, this will populate the main textarea
|
||||
- Check the details, view in the "Preview" tab
|
||||
- What type of release is this?
|
||||
- If it's an RC, then check "Set as a pre-release"
|
||||
- If it's stable, then check "Set as the latest release"
|
||||
- Once you're happy with the contents and ready to save...
|
||||
- If you need more time to review, press the **"Save draft"** button and you can come back to it later
|
||||
- If you are ready to make the release notes public, then press **"Publish release"** button! :tada:
|
||||
|
||||
> If you're curious about how the content is generated, take a look at the `release.yml` configuration:
|
||||
> https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/.github/release.yml
|
||||
14
src/Umbraco.Web.UI.Client/.github/dependabot.yml
vendored
Normal file
14
src/Umbraco.Web.UI.Client/.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
# Maintain dependencies for GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
# Maintain dependencies for npm
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
BIN
src/Umbraco.Web.UI.Client/.github/images/contributing/published-cache-status-dashboard.png
vendored
Normal file
BIN
src/Umbraco.Web.UI.Client/.github/images/contributing/published-cache-status-dashboard.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
src/Umbraco.Web.UI.Client/.github/images/contributing/refresh-status.png
vendored
Normal file
BIN
src/Umbraco.Web.UI.Client/.github/images/contributing/refresh-status.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
src/Umbraco.Web.UI.Client/.github/images/contributing/status-of-cache.png
vendored
Normal file
BIN
src/Umbraco.Web.UI.Client/.github/images/contributing/status-of-cache.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
240
src/Umbraco.Web.UI.Client/.github/localization_overview.md
vendored
Normal file
240
src/Umbraco.Web.UI.Client/.github/localization_overview.md
vendored
Normal file
@@ -0,0 +1,240 @@
|
||||
|
||||
# Help us with Localization!
|
||||
|
||||
Localization of the New Backoffice is in full swing!
|
||||
This is a work in process and here you can find the overview of all the sections that needs to be localized. We are also looking forward to see any contributions towards localization of the new Backoffice.
|
||||
|
||||
|
||||
You may tick a section/subsection in the same PR as your changes, if it completes said section.
|
||||
|
||||
|
||||
Before you start:
|
||||
- Make sure you have read the [README](https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/.github/README.md) and [Contributing Guidelines](https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/.github/CONTRIBUTING.md).
|
||||
- Please note some sections may already be partly or fully localized without it being reflected in the overview just yet.
|
||||
- Get an understanding of how we do localization in the new Backoffice. The explanations can be found in the stories under **Localization** by running `npm run storybook`. Alternatively see the raw story file [localization.mdx](https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/src/packages/core/localization/stories/localization.mdx)
|
||||
|
||||
|
||||
|
||||
# Overview
|
||||
- [Sections that needs to be localized](#sections)
|
||||
- [Keys that needs to be localized](#keys)
|
||||
|
||||
## Sections
|
||||
|
||||
- [ ] [Header App](#header-app)
|
||||
- [ ] [Content](#content)
|
||||
- [ ] [Media](#media)
|
||||
- [ ] [Settings](#settings)
|
||||
- [ ] [Members](#members)
|
||||
- [ ] [Packages](#packages)
|
||||
- [ ] [Dictionary](#dictionary)
|
||||
- [ ] [Users](#users)
|
||||
- [ ] [Property Editors](#property-editor-ui-and-their-input)
|
||||
- [ ] [Modals](#modals)
|
||||
- [ ] [Misc](#misc)
|
||||
|
||||
|
||||
### Subsections
|
||||
|
||||
#### Header App
|
||||
- [ ] Ensure all sections are localized
|
||||
- [ ] Search
|
||||
- [ ] Current user (Modal)
|
||||
- [ ] Change password
|
||||
|
||||
#### Content
|
||||
- [ ] Dashboards
|
||||
- [ ] Welcome
|
||||
- [ ] Redirect Management
|
||||
- [ ] Content / Document
|
||||
- [ ] Section: Content
|
||||
- [ ] Section: Info
|
||||
- [ ] Section: Actions
|
||||
|
||||
#### Media
|
||||
- [ ] (To be continued)
|
||||
|
||||
#### Settings
|
||||
- [ ] Dashboards
|
||||
- [x] Welcome / Settings
|
||||
- [ ] Examine Management
|
||||
- [ ] Models Builder
|
||||
- [ ] Published Status
|
||||
- [ ] Health Check
|
||||
- [x] Profiling
|
||||
- [x] Telemetry Data
|
||||
- [ ] Document Type
|
||||
- [ ] Section: Design
|
||||
- [ ] Section: Structure
|
||||
- [ ] Section: Settings
|
||||
- [ ] Section: Templates
|
||||
- [ ] Media Type
|
||||
- [ ] Member Type
|
||||
- [ ] Data Type
|
||||
- [ ] Section: Details
|
||||
- [ ] Section: Info
|
||||
- [ ] Relation Types
|
||||
- [ ] Log Viewer
|
||||
- [x] Document Blueprints
|
||||
- [ ] Languages
|
||||
- [ ] Extensions
|
||||
- [ ] Templates
|
||||
- [ ] Partial Views
|
||||
- [ ] Stylesheets
|
||||
- [ ] Section: Rich Text Editor
|
||||
- [ ] Section: Code
|
||||
- [ ] Scripts
|
||||
|
||||
#### Members
|
||||
- [ ] Member Groups
|
||||
- [ ] Members
|
||||
|
||||
#### Packages
|
||||
- [ ] Section: Installed
|
||||
- [ ] Section: Created
|
||||
- [ ] Package builder: "Create Package"
|
||||
|
||||
#### Dictionary
|
||||
- [ ] Everything within Dictionary
|
||||
|
||||
#### Users
|
||||
- [ ] Users
|
||||
- [ ] User Groups
|
||||
- [ ] Create user
|
||||
- [ ] User Profiles
|
||||
|
||||
#### Property Editor UI (and their inputs)
|
||||
Ensure all property editors are properly localized.
|
||||
(Some may be missing in this list / more to be added)
|
||||
- [ ] Block Grid
|
||||
- [ ] Block List
|
||||
- [x] Checkbox List
|
||||
- [ ] Collection View
|
||||
- [ ] Color Picker
|
||||
- [ ] Date Picker
|
||||
- [x] Dropdown
|
||||
- [ ] Eye Dropper
|
||||
- [x] Icon Picker
|
||||
- [ ] Image Cropper
|
||||
- [ ] Image Crops Configuration
|
||||
- [x] Label
|
||||
- [ ] Markdown Editor
|
||||
- [ ] Media Picker
|
||||
- [ ] Member Group Picker
|
||||
- [ ] Member Picker
|
||||
- [ ] Multi URL Picker
|
||||
- [ ] Multiple Text String
|
||||
- [ ] Number (missing label)
|
||||
- [ ] Number Range
|
||||
- [ ] Order Direction
|
||||
- [x] Radio Button List
|
||||
- [ ] Slider (label)
|
||||
- [ ] TextBox (label)
|
||||
- [ ] TextArea
|
||||
- [ ] TinyMCE
|
||||
- [ ] Toggle
|
||||
- [ ] Tree Picker
|
||||
- [ ] StartNode
|
||||
- [x] DynamicRoot
|
||||
- [ ] Upload Field
|
||||
- [ ] User Picker
|
||||
- [ ] Value Type
|
||||
|
||||
#### Modals
|
||||
Ensure all modals are properly localized.
|
||||
(Some may be missing in this list / more to be added)
|
||||
- [ ] Code Editor
|
||||
- [ ] Confirm
|
||||
- [ ] Embedded Media
|
||||
- [ ] Folder
|
||||
- [ ] Icon Picker
|
||||
- [ ] Link Picker
|
||||
- [ ] Property Settings
|
||||
- [ ] Section Picker
|
||||
- [ ] Template
|
||||
- [ ] Tree Picker
|
||||
- [ ] Debug
|
||||
|
||||
Rest of modals can be found:
|
||||
- [ ] Umb***ModalName***ModalElement
|
||||
|
||||
|
||||
#### Misc
|
||||
|
||||
- [ ] Tree
|
||||
- [ ] Tree Actions
|
||||
- [ ] Recycle Bin
|
||||
- [ ] Validator messages
|
||||
|
||||
|
||||
## Keys
|
||||
|
||||
Do you speak any of the following languages?
|
||||
Then we need your help! With Bellissima we added new localization keys, and we still need them available in all our supported languages.
|
||||
|
||||
- `bs-BS` - Bosnian (Bosnia and Herzegovina)
|
||||
- `cs-CZ` - Czech (Czech Republic)
|
||||
- `cy-GB` - Welsh (United Kingdom)
|
||||
- `da-DK` - Danish (Denmark)
|
||||
- `de-DE` - German (Germany)
|
||||
- `en-GB` - English (United Kingdom)
|
||||
- `es-ES` - Spanish (Spain)
|
||||
- `fr-FR` - French (France)
|
||||
- `he-IL` - Hebrew (Israel)
|
||||
- `hr-HR` - Croatian (Croatia)
|
||||
- `it-IT` - Italian (Italy)
|
||||
- `ja-JP` - Japanese (Japan)
|
||||
- `ko-KR` - Korean (Korea)
|
||||
- `nb-NO` - Norwegian Bokmål (Norway)
|
||||
- `nl-NL` - Dutch (Netherlands)
|
||||
- `pl-PL` - Polish (Poland)
|
||||
- `pt-BR` - Portuguese (Brazil)
|
||||
- `ro-RO` - Romanian (Romania)
|
||||
- `ru-RU` - Russian (Russia)
|
||||
- `sv-SE` - Swedish (Sweden)
|
||||
- `tr-TR` - Turkish (Turkey)
|
||||
- `ua-UA` - Ukrainian (Ukraine)
|
||||
- `zh-CN` - Chinese (China)
|
||||
- `zh-TW` - Chinese (Taiwan)
|
||||
|
||||
#### settingsDashboard
|
||||
- documentationHeader
|
||||
- documentationDescription
|
||||
- communityHeader
|
||||
- communityDescription
|
||||
- trainingHeader
|
||||
- trainingDescription
|
||||
- supportHeader
|
||||
- supportDescription
|
||||
- videosHeader
|
||||
- videosDescription
|
||||
- getHelp
|
||||
- getCertified
|
||||
- goForum
|
||||
- chatWithCommunity
|
||||
- watchVideos
|
||||
|
||||
- [ ] `bs-BS` - Bosnian (Bosnia and Herzegovina)
|
||||
- [ ] `cs-CZ` - Czech (Czech Republic)
|
||||
- [ ] `cy-GB` - Welsh (United Kingdom)
|
||||
- [x] `da-DK` - Danish (Denmark)
|
||||
- [ ] `de-DE` - German (Germany)
|
||||
- [ ] `en-GB` - English (United Kingdom)
|
||||
- [ ] `es-ES` - Spanish (Spain)
|
||||
- [ ] `fr-FR` - French (France)
|
||||
- [ ] `he-IL` - Hebrew (Israel)
|
||||
- [ ] `hr-HR` - Croatian (Croatia)
|
||||
- [ ] `it-IT` - Italian (Italy)
|
||||
- [ ] `ja-JP` - Japanese (Japan)
|
||||
- [ ] `ko-KR` - Korean (Korea)
|
||||
- [ ] `nb-NO` - Norwegian Bokmål (Norway)
|
||||
- [ ] `nl-NL` - Dutch (Netherlands)
|
||||
- [ ] `pl-PL` - Polish (Poland)
|
||||
- [ ] `pt-BR` - Portuguese (Brazil)
|
||||
- [ ] `ro-RO` - Romanian (Romania)
|
||||
- [ ] `ru-RU` - Russian (Russia)
|
||||
- [ ] `sv-SE` - Swedish (Sweden)
|
||||
- [ ] `tr-TR` - Turkish (Turkey)
|
||||
- [ ] `ua-UA` - Ukrainian (Ukraine)
|
||||
- [ ] `zh-CN` - Chinese (China)
|
||||
- [ ] `zh-TW` - Chinese (Taiwan)
|
||||
30
src/Umbraco.Web.UI.Client/.github/pull_request_template
vendored
Normal file
30
src/Umbraco.Web.UI.Client/.github/pull_request_template
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
<!--- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe the changes in detail -->
|
||||
|
||||
## Types of changes
|
||||
|
||||
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Chore (minor updates related to the tooling or maintenance of the repository, does not impact compiled assets)
|
||||
|
||||
## Motivation and context
|
||||
|
||||
<!--- Why is this change required? What problem does it solve? -->
|
||||
|
||||
## How to test?
|
||||
|
||||
## Screenshots (if appropriate)
|
||||
|
||||
## Checklist
|
||||
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
|
||||
- [ ] If my change requires a change to the documentation, I have updated the documentation in this pull request.
|
||||
- [ ] I have read the **[CONTRIBUTING](<(https://github.com/umbraco/Umbraco.CMS.Backoffice/blob/main/.github/CONTRIBUTING.md)>)** document.
|
||||
- [ ] I have added tests to cover my changes.
|
||||
40
src/Umbraco.Web.UI.Client/.github/release.yml
vendored
Normal file
40
src/Umbraco.Web.UI.Client/.github/release.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# .github/release.yml
|
||||
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
- duplicate
|
||||
- wontfix
|
||||
categories:
|
||||
- title: 🙌 Notable Changes
|
||||
labels:
|
||||
- category/notable
|
||||
- title: 💥 Breaking Changes
|
||||
labels:
|
||||
- category/breaking
|
||||
- title: 🚀 New Features
|
||||
labels:
|
||||
- type/feature
|
||||
- category/feature
|
||||
- type/enhancement
|
||||
- category/enhancement
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- type/bug
|
||||
- category/bug
|
||||
- title: 📄 Documentation
|
||||
labels:
|
||||
- documentation
|
||||
- title: 🏠 Internal
|
||||
labels:
|
||||
- internal
|
||||
- title: 📦 Dependencies
|
||||
labels:
|
||||
- dependencies
|
||||
- title: 🌈 A11Y
|
||||
labels:
|
||||
- accessibility
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- '*'
|
||||
82
src/Umbraco.Web.UI.Client/.github/workflows/azure-static-web-apps-ambitious-stone-0033b3603.yml
vendored
Normal file
82
src/Umbraco.Web.UI.Client/.github/workflows/azure-static-web-apps-ambitious-stone-0033b3603.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Storybook CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_number:
|
||||
type: number
|
||||
description: 'Issue/PR Number to comment on'
|
||||
required: false
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
jobs:
|
||||
build_and_deploy_job:
|
||||
if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.action != 'closed' && contains(github.event.pull_request.labels.*.name, 'storybook'))
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and Deploy Job
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Build And Deploy
|
||||
id: builddeploy
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AMBITIOUS_STONE_0033B3603 }}
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
|
||||
action: 'upload'
|
||||
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
|
||||
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
|
||||
app_location: '/' # App source code path
|
||||
app_build_command: 'npm run build-storybook'
|
||||
api_location: '' # Api source code path - optional
|
||||
output_location: '/storybook-static' # Built app content directory - optional
|
||||
###### End of Repository/Build Configurations ######
|
||||
- name: Comment on PR
|
||||
# azure/static-web-apps-deploy doesn't support workflow_dispatch, so we need to manually comment on the PR
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.issue_number != null
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ inputs.issue_number }}
|
||||
SITE_URL: ${{ steps.builddeploy.outputs.static_web_app_url }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: process.env.ISSUE_NUMBER,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['storybook']
|
||||
})
|
||||
github.rest.issues.createComment({
|
||||
issue_number: process.env.ISSUE_NUMBER,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `Storybook is available at: ${process.env.SITE_URL}`
|
||||
})
|
||||
|
||||
close_pull_request_job:
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'storybook')
|
||||
runs-on: ubuntu-latest
|
||||
name: Close Pull Request Job
|
||||
steps:
|
||||
- name: Close Pull Request
|
||||
id: closepullrequest
|
||||
uses: Azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
app_location: '/'
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_AMBITIOUS_STONE_0033B3603 }}
|
||||
action: 'close'
|
||||
63
src/Umbraco.Web.UI.Client/.github/workflows/build_test.yml
vendored
Normal file
63
src/Umbraco.Web.UI.Client/.github/workflows/build_test.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Build and test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
|
||||
# Allows GitHub to use this workflow to validate the merge queue
|
||||
merge_group:
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: ./package-lock.json
|
||||
- run: npm ci --no-audit --no-fund --prefer-offline
|
||||
- run: npm run lint:errors
|
||||
- run: npm run build:for:cms
|
||||
- run: npm run check:paths
|
||||
- run: npm run generate:jsonschema:dist
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
cache-dependency-path: ./package-lock.json
|
||||
- run: npm ci --no-audit --no-fund --prefer-offline
|
||||
- run: npx playwright install --with-deps
|
||||
- run: npm test
|
||||
- name: Upload Code Coverage reports
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: code-coverage
|
||||
path: coverage/
|
||||
retention-days: 30
|
||||
61
src/Umbraco.Web.UI.Client/.github/workflows/codeql.yml
vendored
Normal file
61
src/Umbraco.Web.UI.Client/.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
- v*/dev
|
||||
schedule:
|
||||
- cron: '33 2 * * 1'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript']
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
20
src/Umbraco.Web.UI.Client/.github/workflows/dependency-review.yml
vendored
Normal file
20
src/Umbraco.Web.UI.Client/.github/workflows/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
82
src/Umbraco.Web.UI.Client/.github/workflows/disabled/npm-publish-github-packages.yml
vendored
Normal file
82
src/Umbraco.Web.UI.Client/.github/workflows/disabled/npm-publish-github-packages.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
# This workflow will publish the @umbraco-cms/backoffice package to npmjs.com
|
||||
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
||||
# The @umbraco-cms scope is owned by Umbraco HQ
|
||||
|
||||
name: Node.js Package
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'devops/publish/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'tsconfig.json'
|
||||
- 'staticwebapp.config.json'
|
||||
- 'README.md'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: Branch or tag or SHA to publish
|
||||
required: false
|
||||
version:
|
||||
description: 'Version to publish'
|
||||
required: false
|
||||
tag:
|
||||
description: 'Tag to publish'
|
||||
required: false
|
||||
type: choice
|
||||
options:
|
||||
- 'next'
|
||||
- 'latest'
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=16384
|
||||
|
||||
jobs:
|
||||
build_publish:
|
||||
name: Build and publish
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: npm-publish
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
registry-url: https://registry.npmjs.org/
|
||||
scope: '@umbraco-cms'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: Calculate version
|
||||
run: |
|
||||
if [ -z "${{inputs.version}}" ]; then
|
||||
echo "No version input, calculating version from package.json"
|
||||
SHA_SHORT=$(echo $GITHUB_SHA | cut -c1-8)
|
||||
VERSION=$(node -p "require('./package.json').version")-$SHA_SHORT
|
||||
echo "Version: $VERSION"
|
||||
echo "BACKOFFICE_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "Version input found, using ${{inputs.version}}"
|
||||
echo "BACKOFFICE_VERSION=${{inputs.version}}" >> "$GITHUB_ENV"
|
||||
fi
|
||||
- name: Publish
|
||||
run: |
|
||||
npm whoami
|
||||
npm version $BACKOFFICE_VERSION --allow-same-version --no-git-tag-version
|
||||
npm publish --tag $BACKOFFICE_NPM_TAG --access public
|
||||
echo "### Published new version of @umbraco-cms/backoffice to npm! :rocket:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Version: $BACKOFFICE_VERSION" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Tag: $BACKOFFICE_NPM_TAG" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Commit: $GITHUB_SHA" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Commit message: $GITHUB_SHA_MESSAGE" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Commit date: $GITHUB_SHA_DATE" >> $GITHUB_STEP_SUMMARY
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
BACKOFFICE_NPM_TAG: ${{ inputs.tag || 'next' }}
|
||||
48
src/Umbraco.Web.UI.Client/.github/workflows/pr-first-response.yml
vendored
Normal file
48
src/Umbraco.Web.UI.Client/.github/workflows/pr-first-response.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: pr-first-response
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
send-response:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Fetch random comment 🗣️ and add it to the PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const response = await fetch('https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment', {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
repo: '${{ github.repository }}',
|
||||
number: '${{ github.event.number }}',
|
||||
actor: '${{ github.actor }}',
|
||||
commentType: 'opened-pr-first-comment'
|
||||
}),
|
||||
headers: {
|
||||
'Authorization': 'Bearer ${{ secrets.OUR_BOT_API_TOKEN }}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await response.text();
|
||||
|
||||
if(response.status === 200 && data !== '') {
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: data
|
||||
});
|
||||
} else {
|
||||
console.log("Status code did not indicate success:", response.status);
|
||||
console.log("Returned data:", data);
|
||||
}
|
||||
} catch(error) {
|
||||
console.log(error);
|
||||
}
|
||||
56
src/Umbraco.Web.UI.Client/.gitignore
vendored
Normal file
56
src/Umbraco.Web.UI.Client/.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-cms
|
||||
dist-ssr
|
||||
/types
|
||||
*.tsbuildinfo
|
||||
*.local
|
||||
*.tgz
|
||||
|
||||
## testing
|
||||
coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
!.vscode/settings.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# eslint
|
||||
.eslintcache
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
|
||||
# storybook
|
||||
storybook-static/
|
||||
|
||||
# API Docs
|
||||
ui-api/
|
||||
|
||||
custom-elements.json
|
||||
|
||||
# JSON for HTML Custom Data
|
||||
# https://github.com/runem/web-component-analyzer#vscode
|
||||
# https://github.com/microsoft/vscode-custom-data
|
||||
vscode-html-custom-data.json
|
||||
public/tinymce/*
|
||||
|
||||
# Vite runtime files
|
||||
vite.config.ts.timestamp-*.mjs
|
||||
9
src/Umbraco.Web.UI.Client/.madgerc
Normal file
9
src/Umbraco.Web.UI.Client/.madgerc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"tsConfig": "tsconfig.json",
|
||||
"detectiveOptions": {
|
||||
"ts": {
|
||||
"skipTypeImports": true,
|
||||
"skipAsyncImports": true
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/Umbraco.Web.UI.Client/.npmrc
Normal file
1
src/Umbraco.Web.UI.Client/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
legacy-peer-deps=true
|
||||
1
src/Umbraco.Web.UI.Client/.nvmrc
Normal file
1
src/Umbraco.Web.UI.Client/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
20
|
||||
4
src/Umbraco.Web.UI.Client/.prettierignore
Normal file
4
src/Umbraco.Web.UI.Client/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
||||
# Ignore auto-generated backend-api files
|
||||
src/external/backend-api/src
|
||||
src/packages/core/icon-registry/icons.ts
|
||||
src/packages/core/icon-registry/icons
|
||||
8
src/Umbraco.Web.UI.Client/.prettierrc.json
Normal file
8
src/Umbraco.Web.UI.Client/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"useTabs": true
|
||||
}
|
||||
4
src/Umbraco.Web.UI.Client/.sonarlint/connectedMode.json
Normal file
4
src/Umbraco.Web.UI.Client/.sonarlint/connectedMode.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sonarCloudOrganization": "umbraco",
|
||||
"projectKey": "umbraco_Umbraco.CMS.Backoffice"
|
||||
}
|
||||
57
src/Umbraco.Web.UI.Client/.storybook/main.ts
Normal file
57
src/Umbraco.Web.UI.Client/.storybook/main.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { dirname, join } from 'path';
|
||||
import { StorybookConfig } from '@storybook/web-components-vite';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../@(src|libs|apps|storybook)/**/*.mdx', '../@(src|libs|apps|storybook)/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
getAbsolutePath('@storybook/addon-links'),
|
||||
getAbsolutePath('@storybook/addon-essentials'),
|
||||
getAbsolutePath('@storybook/addon-a11y'),
|
||||
{
|
||||
name: '@storybook/addon-docs',
|
||||
options: {
|
||||
mdxPluginOptions: {
|
||||
mdxCompileOptions: {
|
||||
remarkPlugins: [remarkGfm],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: {
|
||||
name: getAbsolutePath('@storybook/web-components-vite'),
|
||||
options: {},
|
||||
},
|
||||
staticDirs: [
|
||||
'../public-assets',
|
||||
'../public',
|
||||
'../src/assets',
|
||||
{
|
||||
from: '../src/packages/core/icon-registry/icons',
|
||||
to: 'assets/icons',
|
||||
},
|
||||
],
|
||||
typescript: {
|
||||
check: true,
|
||||
},
|
||||
docs: {},
|
||||
managerHead(head, { configType }) {
|
||||
const base = process.env.VITE_BASE_PATH || '/';
|
||||
const injections = [
|
||||
`<base href="${base}" />`, // This decide how storybook's main frame visit stories
|
||||
];
|
||||
return configType === 'PRODUCTION' ? `${injections.join('')}${head}` : head;
|
||||
},
|
||||
refs: {
|
||||
uui: {
|
||||
title: 'Umbraco UI Library',
|
||||
url: 'https://62189360eeb21b003ab2f4ad-vfnpsanjps.chromatic.com/',
|
||||
},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
function getAbsolutePath(value: string): any {
|
||||
return dirname(require.resolve(join(value, 'package.json')));
|
||||
}
|
||||
5
src/Umbraco.Web.UI.Client/.storybook/manager.ts
Normal file
5
src/Umbraco.Web.UI.Client/.storybook/manager.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { addons } from '@storybook/manager-api';
|
||||
|
||||
addons.setConfig({
|
||||
enableShortcuts: false,
|
||||
});
|
||||
3
src/Umbraco.Web.UI.Client/.storybook/package.json
Normal file
3
src/Umbraco.Web.UI.Client/.storybook/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
4
src/Umbraco.Web.UI.Client/.storybook/preview-body.html
Normal file
4
src/Umbraco.Web.UI.Client/.storybook/preview-body.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
document.body.classList.add('uui-font');
|
||||
document.body.classList.add('uui-text');
|
||||
</script>
|
||||
43
src/Umbraco.Web.UI.Client/.storybook/preview-head.html
Normal file
43
src/Umbraco.Web.UI.Client/.storybook/preview-head.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<style>
|
||||
/* Make stories able to use 100% height */
|
||||
html {
|
||||
height: 100vh;
|
||||
}
|
||||
/* Make stories able to use 100% height */
|
||||
body,
|
||||
#root,
|
||||
#root-inner {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
background-color: var(--uui-color-background);
|
||||
padding: 1.5em;
|
||||
border-radius: 3px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
</style>
|
||||
<script src="umbraco/backoffice/msw/index.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
window.addEventListener('load', () => {
|
||||
var body = document.querySelector('body');
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey === true && e.key === 'g') {
|
||||
if (body.hasAttribute('baseline-grid')) {
|
||||
body.removeAttribute('baseline-grid');
|
||||
} else {
|
||||
body.setAttribute('baseline-grid', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
148
src/Umbraco.Web.UI.Client/.storybook/preview.js
Normal file
148
src/Umbraco.Web.UI.Client/.storybook/preview.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import '@umbraco-ui/uui-css/dist/uui-css.css';
|
||||
import '../src/css/umb-css.css';
|
||||
|
||||
import 'element-internals-polyfill';
|
||||
import '@umbraco-ui/uui';
|
||||
|
||||
import { html } from 'lit';
|
||||
import { setCustomElements } from '@storybook/web-components';
|
||||
|
||||
import { startMockServiceWorker } from '../src/mocks';
|
||||
|
||||
import '../src/libs/controller-api/controller-host-provider.element';
|
||||
import { UmbModalManagerContext } from '../src/packages/core/modal';
|
||||
import { UmbDataTypeTreeStore } from '../src/packages/data-type/tree/data-type-tree.store';
|
||||
import { UmbDocumentDetailStore } from '../src/packages/documents/documents/repository/detail/document-detail.store';
|
||||
import { UmbDocumentTreeStore } from '../src/packages/documents/documents/tree/document-tree.store';
|
||||
import { UmbCurrentUserStore } from '../src/packages/user/current-user/repository/current-user.store';
|
||||
import { umbExtensionsRegistry } from '../src/packages/core/extension-registry';
|
||||
import { UmbIconRegistry } from '../src/packages/core/icon-registry/icon.registry';
|
||||
import { UmbLitElement } from '../src/packages/core/lit-element';
|
||||
import { umbLocalizationRegistry } from '../src/packages/core/localization';
|
||||
import customElementManifests from '../dist-cms/custom-elements.json';
|
||||
import icons from '../src/packages/core/icon-registry/icons';
|
||||
|
||||
import '../src/libs/context-api/provide/context-provider.element';
|
||||
import '../src/packages/core/components';
|
||||
|
||||
import { manifests as documentManifests } from '../src/packages/documents/manifests';
|
||||
import { manifests as localizationManifests } from '../src/packages/core/localization/manifests';
|
||||
import { UmbNotificationContext } from '../src/packages/core/notification';
|
||||
|
||||
// MSW
|
||||
startMockServiceWorker({ serviceWorker: { url: (import.meta.env.VITE_BASE_PATH ?? '/') + 'mockServiceWorker.js' } });
|
||||
|
||||
class UmbStoryBookElement extends UmbLitElement {
|
||||
_umbIconRegistry = new UmbIconRegistry();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._umbIconRegistry.setIcons(icons);
|
||||
this._umbIconRegistry.attach(this);
|
||||
this._registerExtensions(documentManifests);
|
||||
new UmbModalManagerContext(this);
|
||||
new UmbCurrentUserStore(this);
|
||||
new UmbNotificationContext(this);
|
||||
|
||||
this._registerExtensions(localizationManifests);
|
||||
umbLocalizationRegistry.loadLanguage('en-us'); // register default language
|
||||
}
|
||||
|
||||
_registerExtensions(manifests) {
|
||||
manifests.forEach((manifest) => {
|
||||
if (umbExtensionsRegistry.isRegistered(manifest.alias)) return;
|
||||
umbExtensionsRegistry.register(manifest);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>
|
||||
<umb-backoffice-modal-container></umb-backoffice-modal-container>
|
||||
<umb-backoffice-notification-container></umb-backoffice-notification-container>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('umb-storybook', UmbStoryBookElement);
|
||||
|
||||
const storybookProvider = (story) => html` <umb-storybook>${story()}</umb-storybook> `;
|
||||
|
||||
const dataTypeStoreProvider = (story) => html`
|
||||
<umb-controller-host-provider .create=${(host) => new UmbDataTypeTreeStore(host)}
|
||||
>${story()}</umb-controller-host-provider
|
||||
>
|
||||
`;
|
||||
|
||||
const documentStoreProvider = (story) => html`
|
||||
<umb-controller-host-provider .create=${(host) => new UmbDocumentDetailStore(host)}
|
||||
>${story()}</umb-controller-host-provider
|
||||
>
|
||||
`;
|
||||
|
||||
const documentTreeStoreProvider = (story) => html`
|
||||
<umb-controller-host-provider .create=${(host) => new UmbDocumentTreeStore(host)}
|
||||
>${story()}</umb-controller-host-provider
|
||||
>
|
||||
`;
|
||||
|
||||
// Provide the MSW addon decorator globally
|
||||
export const decorators = [documentStoreProvider, documentTreeStoreProvider, dataTypeStoreProvider, storybookProvider];
|
||||
|
||||
export const parameters = {
|
||||
docs: {
|
||||
source: {
|
||||
excludeDecorators: true,
|
||||
format: 'html', // see storybook docs for more info on this format https://storybook.js.org/docs/api/doc-blocks/doc-block-source#format
|
||||
},
|
||||
},
|
||||
options: {
|
||||
storySort: {
|
||||
method: 'alphabetical',
|
||||
includeNames: true,
|
||||
order: [
|
||||
'Guides',
|
||||
[
|
||||
'Getting started',
|
||||
'Extending the Backoffice',
|
||||
[
|
||||
'Intro',
|
||||
'Registration',
|
||||
'Header Apps',
|
||||
'Sections',
|
||||
['Intro', 'Sidebar', 'Views', '*'],
|
||||
'Entity Actions',
|
||||
'Workspaces',
|
||||
['Intro', 'Views', 'Actions', '*'],
|
||||
'Property Editors',
|
||||
'Repositories',
|
||||
'*',
|
||||
],
|
||||
'*',
|
||||
],
|
||||
'*',
|
||||
],
|
||||
},
|
||||
},
|
||||
controls: {
|
||||
expanded: true,
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'Greyish',
|
||||
values: [
|
||||
{
|
||||
name: 'Greyish',
|
||||
value: '#F3F3F5',
|
||||
},
|
||||
{
|
||||
name: 'White',
|
||||
value: '#ffffff',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
setCustomElements(customElementManifests);
|
||||
export const tags = ['autodocs'];
|
||||
14
src/Umbraco.Web.UI.Client/.vscode/extensions.json
vendored
Normal file
14
src/Umbraco.Web.UI.Client/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"gruntfuggly.todo-tree",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"mikestead.dotenv",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"runem.lit-plugin",
|
||||
"esbenp.prettier-vscode",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"vunguyentuan.vscode-css-variables",
|
||||
"unifiedjs.vscode-mdx",
|
||||
"editorconfig.editorconfig"
|
||||
]
|
||||
}
|
||||
30
src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets
vendored
Normal file
30
src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"UmbNewLitElement": {
|
||||
"prefix": "new umb element",
|
||||
"scope": "typescript",
|
||||
"body": [
|
||||
"import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';",
|
||||
"import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';",
|
||||
"import { UmbTextStyles } from '@umbraco-cms/backoffice/style';",
|
||||
"",
|
||||
"@customElement('umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}')",
|
||||
"export class Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/}Element extends UmbLitElement {",
|
||||
"\toverride render() {",
|
||||
"\t\treturn html`$0`;",
|
||||
"\t}",
|
||||
"",
|
||||
"\tstatic override readonly styles = [UmbTextStyles, css``];",
|
||||
"}",
|
||||
"",
|
||||
"export { Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/}Element as element };",
|
||||
"",
|
||||
"declare global {",
|
||||
"\tinterface HTMLElementTagNameMap {",
|
||||
"\t\t'umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}': Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/}Element;",
|
||||
"\t}",
|
||||
"}",
|
||||
"",
|
||||
],
|
||||
"description": "Create a new Umbraco Lit element",
|
||||
},
|
||||
}
|
||||
40
src/Umbraco.Web.UI.Client/.vscode/settings.json
vendored
Normal file
40
src/Umbraco.Web.UI.Client/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"cssVariables.lookupFiles": ["node_modules/@umbraco-ui/uui-css/dist/custom-properties.css"],
|
||||
"cSpell.words": [
|
||||
"backoffice",
|
||||
"Backoffice",
|
||||
"combobox",
|
||||
"ctrls",
|
||||
"devs",
|
||||
"Dropcursor",
|
||||
"Elementable",
|
||||
"Gapcursor",
|
||||
"iframes",
|
||||
"invariantable",
|
||||
"lucide",
|
||||
"Niels",
|
||||
"pickable",
|
||||
"popovertarget",
|
||||
"Registrator",
|
||||
"Routable",
|
||||
"stylesheet",
|
||||
"svgs",
|
||||
"templating",
|
||||
"tinymce",
|
||||
"tiptap",
|
||||
"umbraco",
|
||||
"Uncategorized",
|
||||
"uninitialize",
|
||||
"unprovide",
|
||||
"unpublishing",
|
||||
"variantable"
|
||||
],
|
||||
"exportall.config.folderListener": [],
|
||||
"exportall.config.relExclusion": [],
|
||||
"conventionalCommits.scopes": ["partial views"],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "umbraco",
|
||||
"projectKey": "umbraco_Umbraco.CMS.Backoffice"
|
||||
}
|
||||
}
|
||||
44
src/Umbraco.Web.UI.Client/LICENSE
Normal file
44
src/Umbraco.Web.UI.Client/LICENSE
Normal file
@@ -0,0 +1,44 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Umbraco HQ
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
Third-party licenses
|
||||
|
||||
---
|
||||
|
||||
Lucide License
|
||||
ISC License <https://lucide.dev/license>
|
||||
|
||||
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
Simple Icons
|
||||
CC0 1.0 Universal license <https://creativecommons.org/publicdomain/zero/1.0/>
|
||||
|
||||
The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
|
||||
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.
|
||||
172
src/Umbraco.Web.UI.Client/README.md
Normal file
172
src/Umbraco.Web.UI.Client/README.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# @umbraco-cms/backoffice
|
||||
|
||||
This package contains the types for the Umbraco Backoffice.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install -D @umbraco-cms/backoffice
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Vanilla JavaScript
|
||||
|
||||
Create an umbraco-package.json file in the root of your package.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My.Package",
|
||||
"version": "0.1.0",
|
||||
"extensions": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"alias": "my.custom.dashboard",
|
||||
"name": "My Dashboard",
|
||||
"js": "/App_Plugins/MyPackage/dashboard.js",
|
||||
"weight": -1,
|
||||
"meta": {
|
||||
"label": "My Dashboard",
|
||||
"pathname": "my-dashboard"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"alias": "Umb.Condition.SectionAlias",
|
||||
"match": "Umb.Section.Content"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Then create a dashboard.js file the same folder.
|
||||
|
||||
```javascript
|
||||
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
|
||||
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
|
||||
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<uui-box>
|
||||
<h1>Welcome to my dashboard</h1>
|
||||
<p>Example of vanilla JS code</p>
|
||||
|
||||
<uui-button label="Click me" id="clickMe" look="secondary"></uui-button>
|
||||
</uui-box>
|
||||
`;
|
||||
|
||||
export default class MyDashboardElement extends UmbElementMixin(HTMLElement) {
|
||||
/** @type {import('@umbraco-cms/backoffice/notification').UmbNotificationContext} */
|
||||
#notificationContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
||||
|
||||
this.shadowRoot.getElementById('clickMe').addEventListener('click', this.onClick.bind(this));
|
||||
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => {
|
||||
this.#notificationContext = _instance;
|
||||
});
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
this.#notificationContext?.peek('positive', { data: { headline: 'Hello' } });
|
||||
};
|
||||
}
|
||||
|
||||
customElements.define('my-custom-dashboard', MyDashboardElement);
|
||||
```
|
||||
|
||||
### TypeScript with Lit
|
||||
|
||||
First install Lit and Vite. This command will create a new folder called `my-package` which will have the Vite tooling and Lit for WebComponent development setup.
|
||||
|
||||
```bash
|
||||
npm create vite@latest -- --template lit-ts my-package
|
||||
```
|
||||
|
||||
Go to the new folder and install the backoffice package.
|
||||
|
||||
```bash
|
||||
cd my-package
|
||||
npm install -D @umbraco-cms/backoffice
|
||||
```
|
||||
|
||||
Then go to the element located in `src/my-element.ts` and replace it with the following code.
|
||||
|
||||
```typescript
|
||||
// src/my-element.ts
|
||||
import { LitElement, html, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
|
||||
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
|
||||
|
||||
@customElement('my-element')
|
||||
export default class MyElement extends UmbElementMixin(LitElement) {
|
||||
private _notificationContext?: UmbNotificationContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (_instance) => {
|
||||
this._notificationContext = _instance;
|
||||
});
|
||||
}
|
||||
|
||||
onClick() {
|
||||
this._notificationContext?.peek('positive', { data: { message: '#h5yr' } });
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-box headline="Welcome">
|
||||
<p>A TypeScript Lit Dashboard</p>
|
||||
<uui-button look="primary" label="Click me" @click=${() => this.onClick()}></uui-button>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'my-element': MyElement;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally add an umbraco-package.json file in the root of your package folder `my-package`.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My.Package",
|
||||
"version": "0.1.0",
|
||||
"extensions": [
|
||||
{
|
||||
"type": "dashboard",
|
||||
"alias": "my.custom.dashboard",
|
||||
"name": "My Dashboard",
|
||||
"js": "/App_Plugins/MyPackage/dist/my-package.js",
|
||||
"weight": -1,
|
||||
"meta": {
|
||||
"label": "My Dashboard",
|
||||
"pathname": "my-dashboard"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"alias": "Umb.Condition.SectionAlias",
|
||||
"match": "Umb.Section.Content"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
80
src/Umbraco.Web.UI.Client/devops/build/check-path-length.js
Normal file
80
src/Umbraco.Web.UI.Client/devops/build/check-path-length.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const PROJECT_DIR = process.argv[2] ?? '.';
|
||||
const MAX_PATH_LENGTH = process.argv[3] ?? 140;
|
||||
const IS_CI = process.env.CI === 'true';
|
||||
const IS_AZURE_PIPELINES = process.env.TF_BUILD === 'true';
|
||||
const IS_GITHUB_ACTIONS = process.env.GITHUB_ACTIONS === 'true';
|
||||
const FILE_PATH_COLOR = '\x1b[36m%s\x1b[0m';
|
||||
const ERROR_COLOR = '\x1b[31m%s\x1b[0m';
|
||||
const SUCCESS_COLOR = '\x1b[32m%s\x1b[0m';
|
||||
const processExitCode = 1; // Default to 1 to fail the build, 0 to just log the issues
|
||||
|
||||
console.log(`Checking path length in ${PROJECT_DIR} for paths exceeding ${MAX_PATH_LENGTH}...`);
|
||||
console.log('CI detected:', IS_CI);
|
||||
|
||||
console.log('\n-----------------------------------');
|
||||
console.log('Results:');
|
||||
console.log('-----------------------------------\n');
|
||||
|
||||
const hasError = checkPathLength(PROJECT_DIR);
|
||||
|
||||
if (hasError) {
|
||||
console.log('\n-----------------------------------');
|
||||
console.log(ERROR_COLOR, 'Path length check failed');
|
||||
console.log('-----------------------------------\n');
|
||||
if (IS_CI && processExitCode) {
|
||||
process.exit(processExitCode);
|
||||
}
|
||||
} else {
|
||||
console.log('\n-----------------------------------');
|
||||
console.log(SUCCESS_COLOR, 'Path length check passed');
|
||||
console.log('-----------------------------------\n');
|
||||
}
|
||||
|
||||
// Functions
|
||||
|
||||
/**
|
||||
* Recursively check the path length of all files in a directory.
|
||||
* @param {string} dir - The directory to check for path lengths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function checkPathLength(dir) {
|
||||
const files = readdirSync(dir);
|
||||
let hasError = false;
|
||||
|
||||
files.forEach(file => {
|
||||
const filePath = join(dir, file);
|
||||
if (filePath.length > MAX_PATH_LENGTH) {
|
||||
hasError = true;
|
||||
|
||||
if (IS_AZURE_PIPELINES) {
|
||||
console.error(`##vso[task.logissue type=error;sourcepath=${mapFileToSourcePath(filePath)};]Path exceeds maximum length of ${MAX_PATH_LENGTH} characters: ${filePath} with ${filePath.length} characters`);
|
||||
} else if (IS_GITHUB_ACTIONS) {
|
||||
console.error(`::error file=${mapFileToSourcePath(filePath)},title=Path exceeds ${MAX_PATH_LENGTH} characters::Paths should not be longer than ${MAX_PATH_LENGTH} characters to support WIN32 systems. The file ${filePath} exceeds that with ${filePath.length - MAX_PATH_LENGTH} characters.`);
|
||||
} else {
|
||||
console.error(FILE_PATH_COLOR, mapFileToSourcePath(filePath), '(exceeds by', filePath.length - MAX_PATH_LENGTH, 'chars)');
|
||||
}
|
||||
}
|
||||
|
||||
if (statSync(filePath).isDirectory()) {
|
||||
const subHasError = checkPathLength(filePath);
|
||||
if (subHasError) {
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return hasError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a file path to a source path for CI logs.
|
||||
* @remark This might not always work as expected, especially on bundled files, but it's a best effort to map the file path to a source path.
|
||||
* @param {string} file - The file path to map to a source path
|
||||
* @returns {string}
|
||||
*/
|
||||
function mapFileToSourcePath(file) {
|
||||
return file.replace(PROJECT_DIR, 'src').replace('.js', '.ts');
|
||||
}
|
||||
9
src/Umbraco.Web.UI.Client/devops/build/copy-to-cms.js
Normal file
9
src/Umbraco.Web.UI.Client/devops/build/copy-to-cms.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { cpSync, rmSync } from 'fs';
|
||||
|
||||
const srcDir = './dist-cms';
|
||||
const outputDir = '../Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice';
|
||||
|
||||
rmSync(outputDir, { recursive: true, force: true });
|
||||
cpSync(srcDir, outputDir, { recursive: true });
|
||||
|
||||
console.log('--- Copied build output to CMS successfully. ---');
|
||||
@@ -0,0 +1,23 @@
|
||||
import { createImportMap } from "../importmap/index.js";
|
||||
import { writeFileSync, rmSync } from "fs";
|
||||
import { packageJsonName, packageJsonVersion } from "../package/index.js";
|
||||
|
||||
const srcDir = './dist-cms';
|
||||
const outputModuleList = `${srcDir}/umbraco-package.json`;
|
||||
const importmap = createImportMap({ rootDir: '/umbraco/backoffice', additionalImports: {} });
|
||||
|
||||
const umbracoPackageJson = {
|
||||
name: packageJsonName,
|
||||
version: packageJsonVersion,
|
||||
extensions: [],
|
||||
importmap
|
||||
};
|
||||
|
||||
try {
|
||||
rmSync(outputModuleList, { force: true });
|
||||
writeFileSync(outputModuleList, JSON.stringify(umbracoPackageJson));
|
||||
console.log(`Wrote manifest to ${outputModuleList}`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to write manifest to ${outputModuleList}`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
25
src/Umbraco.Web.UI.Client/devops/build/global-types.js
Normal file
25
src/Umbraco.Web.UI.Client/devops/build/global-types.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createImportMap } from '../importmap/index.js';
|
||||
import { writeFileWithDir } from '../utils/index.js';
|
||||
|
||||
const tsPath = './dist-cms/packages/extension-types/index.d.ts';
|
||||
|
||||
const importmap = createImportMap({
|
||||
rootDir: './src',
|
||||
replaceModuleExtensions: true,
|
||||
});
|
||||
|
||||
const paths = Object.keys(importmap.imports);
|
||||
|
||||
const content = `
|
||||
${paths.map((path) => `import '${path}';`).join('\n')}
|
||||
`;
|
||||
|
||||
writeFileWithDir(tsPath, content, (err) => {
|
||||
if (err) {
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log(`global-types file generated`);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensures the use of the `import type` operator from the `src/core/models/index.ts` file.',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
ImportDeclaration: function (node) {
|
||||
if (
|
||||
node.source.parent.importKind !== 'type' &&
|
||||
(node.source.value.endsWith('/models') || node.source.value === 'router-slot/model')
|
||||
) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const nodeSource = sourceCode.getText(node);
|
||||
context.report({
|
||||
node,
|
||||
message: 'Use `import type` instead of `import`.',
|
||||
fix: (fixer) => fixer.replaceText(node, nodeSource.replace('import', 'import type')),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce Element class name to end with "Element".',
|
||||
category: 'Naming',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
ClassDeclaration(node) {
|
||||
// check if the class extends HTMLElement, LitElement, or UmbLitElement
|
||||
const isExtendingElement =
|
||||
node.superClass && ['HTMLElement', 'LitElement', 'UmbLitElement'].includes(node.superClass.name);
|
||||
// check if the class name ends with 'Element'
|
||||
const isClassNameValid = node.id.name.endsWith('Element');
|
||||
|
||||
if (isExtendingElement && !isClassNameValid) {
|
||||
context.report({
|
||||
node,
|
||||
message: "Element class name should end with 'Element'.",
|
||||
// There us no fixer on purpose because it's not safe to rename the class. We want to do that trough the refactoring tool.
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description: 'Enforce Custom Element names to start with "umb-".',
|
||||
category: 'Naming',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
// check if the expression is @customElement decorator
|
||||
const isCustomElementDecorator =
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'customElement' &&
|
||||
node.arguments.length === 1 &&
|
||||
node.arguments[0].type === 'Literal' &&
|
||||
typeof node.arguments[0].value === 'string';
|
||||
|
||||
if (isCustomElementDecorator) {
|
||||
const elementName = node.arguments[0].value;
|
||||
|
||||
// check if the element name starts with 'umb-', or 'test-', to be allow tests to have custom elements:
|
||||
const isElementNameValid = elementName.startsWith('umb-') ? true : elementName.startsWith('test-');
|
||||
|
||||
if (!isElementNameValid) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Custom Element name should start with "umb-".',
|
||||
// There is no fixer on purpose because it's not safe to automatically rename the element name.
|
||||
// Renaming should be done manually with consideration of potential impacts.
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/** @type {import('eslint').Rule.RuleModule}*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Ensures that the application strictly uses node_modules imports from `@umbraco-cms/backoffice/external`. This is needed to run the application in the browser.',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: {
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
maxItems: 1,
|
||||
items: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
exceptions: { type: 'array' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
create: (context) => {
|
||||
return {
|
||||
ImportDeclaration: (node) => {
|
||||
const { source } = node;
|
||||
const { value } = source;
|
||||
|
||||
const options = context.options[0] || {};
|
||||
const exceptions = options.exceptions || [];
|
||||
|
||||
// If import starts with any of the following, then it's allowed
|
||||
if (exceptions.some((v) => value.startsWith(v))) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
'node_modules imports should be proxied through `@umbraco-cms/backoffice/external`. Please create it if it does not exist.',
|
||||
fix: (fixer) =>
|
||||
fixer.replaceText(source, `'@umbraco-cms/backoffice/external${value.startsWith('/') ? '' : '/'}${value}'`),
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
/** @type {import('eslint').Rule.RuleModule}*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensures relative imports use the ".js" file extension.',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
create: (context) => {
|
||||
function correctImport(value) {
|
||||
if (value === '.') {
|
||||
return './index.js';
|
||||
}
|
||||
|
||||
if (
|
||||
value &&
|
||||
value.startsWith('.') &&
|
||||
!value.endsWith('.js') &&
|
||||
!value.endsWith('.css') &&
|
||||
!value.endsWith('.json') &&
|
||||
!value.endsWith('.svg') &&
|
||||
!value.endsWith('.jpg') &&
|
||||
!value.endsWith('.png')
|
||||
) {
|
||||
return (value.endsWith('/') ? value + 'index' : value) + '.js';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration: (node) => {
|
||||
const { source } = node;
|
||||
const { value } = source;
|
||||
|
||||
const fixedValue = correctImport(value);
|
||||
if (fixedValue) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Relative imports should use the ".js" file extension.',
|
||||
fix: (fixer) => fixer.replaceText(source, `'${fixedValue}'`),
|
||||
});
|
||||
}
|
||||
},
|
||||
ImportExpression: (node) => {
|
||||
const { source } = node;
|
||||
const { value } = source;
|
||||
|
||||
const fixedSource = correctImport(value);
|
||||
if (fixedSource) {
|
||||
context.report({
|
||||
node: source,
|
||||
message: 'Relative imports should use the ".js" file extension.',
|
||||
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportAllDeclaration: (node) => {
|
||||
const { source } = node;
|
||||
const { value } = source;
|
||||
|
||||
const fixedSource = correctImport(value);
|
||||
if (fixedSource) {
|
||||
context.report({
|
||||
node: source,
|
||||
message: 'Relative exports should use the ".js" file extension.',
|
||||
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportNamedDeclaration: (node) => {
|
||||
const { source } = node;
|
||||
if (!source) return;
|
||||
const { value } = source;
|
||||
|
||||
const fixedSource = correctImport(value);
|
||||
if (fixedSource) {
|
||||
context.report({
|
||||
node: source,
|
||||
message: 'Relative exports should use the ".js" file extension.',
|
||||
fix: (fixer) => fixer.replaceText(source, `'${fixedSource}'`),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/** @type {import('eslint').Rule.RuleModule}*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Ensure all exported string constants should be in uppercase with words separated by underscores and prefixed with UMB_',
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
excludedFileNames: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create: function (context) {
|
||||
const excludedFileNames = context.options[0]?.excludedFileNames || [];
|
||||
return {
|
||||
ExportNamedDeclaration(node) {
|
||||
const fileName = context.filename;
|
||||
|
||||
if (excludedFileNames.some((excludedFileName) => fileName.includes(excludedFileName))) {
|
||||
// Skip the rule check for files in the excluded list
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.declaration && node.declaration.type === 'VariableDeclaration') {
|
||||
const declaration = node.declaration.declarations[0];
|
||||
const { id, init } = declaration;
|
||||
|
||||
if (id && id.type === 'Identifier' && init && init.type === 'Literal' && typeof init.value === 'string') {
|
||||
const isValidName = /^[A-Z]+(_[A-Z]+)*$/.test(id.name);
|
||||
|
||||
if (!isValidName || !id.name.startsWith('UMB_')) {
|
||||
context.report({
|
||||
node: id,
|
||||
message:
|
||||
'Exported string constant should be in uppercase with words separated by underscores and prefixed with UMB_',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description:
|
||||
'Ensures that any API resources from the `@umbraco-cms/backoffice/external/backend-api` module are not used directly. Instead you should use the `tryExecuteAndNotify` function from the `@umbraco-cms/backoffice/resources` module.',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
// If methods called on *Service classes are not already wrapped with `await tryExecuteAndNotify()`, then we should suggest to wrap them.
|
||||
CallExpression: function (node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.type === 'Identifier' &&
|
||||
node.callee.object.name.endsWith('Service') &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name !== 'constructor'
|
||||
) {
|
||||
const hasTryExecuteAndNotify =
|
||||
node.parent &&
|
||||
node.parent.callee &&
|
||||
(node.parent.callee.name === 'tryExecute' || node.parent.callee.name === 'tryExecuteAndNotify');
|
||||
if (!hasTryExecuteAndNotify) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Wrap this call with `tryExecuteAndNotify()`. Make sure to `await` the result.',
|
||||
fix: (fixer) => [
|
||||
fixer.insertTextBefore(node, 'tryExecuteAndNotify(this, '),
|
||||
fixer.insertTextAfter(node, ')'),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const getDirectories = (source) =>
|
||||
fs
|
||||
.readdirSync(source, { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
|
||||
// TODO: get the correct list of modules. This is a temporary solution where we assume that a directory is equivalent to a module
|
||||
// TODO: include package modules in this list
|
||||
const coreRoot = path.join(__dirname, '../../../', 'src/packages/core');
|
||||
const externalRoot = path.join(__dirname, '../../../', 'src/external');
|
||||
const libsRoot = path.join(__dirname, '../../../', 'src/libs');
|
||||
const coreModules = getDirectories(coreRoot).map((dir) => `/core/${dir}/`);
|
||||
const externalModules = getDirectories(externalRoot).map((dir) => `/${dir}/`);
|
||||
const libsModules = getDirectories(libsRoot).map((dir) => `/${dir}/`);
|
||||
|
||||
const modulePathIdentifiers = [...coreModules, ...externalModules, ...libsModules];
|
||||
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Prevent relative import to a module that is in the import map.',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
unexpectedValue: 'Relative import paths should include "{{value}}".',
|
||||
},
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
// exclude test and story files
|
||||
if (context.filename.endsWith('.test.ts') || context.filename.endsWith('.stories.ts')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const importPath = node.source.value;
|
||||
|
||||
if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
||||
if (modulePathIdentifiers.some((moduleName) => importPath.includes(moduleName))) {
|
||||
context.report({
|
||||
node,
|
||||
message: 'Use the correct import map alias instead of a relative import path: ' + importPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Ensures that the application does not rely on file system paths for imports. Instead, use import aliases or relative imports. This also solves a problem where GitHub fails on the test runner step.',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
ImportDeclaration: function (node) {
|
||||
if (node.source.value.startsWith('src/')) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
'Prefer using import aliases or relative imports instead of absolute imports. Example: `import { MyComponent } from "src/components/MyComponent";` should be `import { MyComponent } from "@components/MyComponent";`',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/** @type {import('eslint').Rule.RuleModule}*/
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Enforce the "styles" property with the static modifier to be the last property of a class that ends with "Element".',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
return {
|
||||
ClassDeclaration(node) {
|
||||
const className = node.id.name;
|
||||
if (className.endsWith('Element')) {
|
||||
const staticStylesProperty = node.body.body.find((bodyNode) => {
|
||||
return bodyNode.type === 'PropertyDefinition' && bodyNode.key.name === 'styles' && bodyNode.static;
|
||||
});
|
||||
if (staticStylesProperty) {
|
||||
const lastProperty = node.body.body[node.body.body.length - 1];
|
||||
if (lastProperty.key.name !== staticStylesProperty.key.name) {
|
||||
context.report({
|
||||
node: staticStylesProperty,
|
||||
message: 'The "styles" property should be the last property of a class declaration.',
|
||||
data: {
|
||||
className: className,
|
||||
},
|
||||
fix: function (fixer) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const staticStylesPropertyText = sourceCode.getText(staticStylesProperty);
|
||||
return [
|
||||
fixer.replaceTextRange(staticStylesProperty.range, ''),
|
||||
fixer.insertTextAfterRange(lastProperty.range, '\n \n ' + staticStylesPropertyText),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/** @type {import('eslint').Rule.RuleModule} */
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure that all class declarations are prefixed with "Umb"',
|
||||
category: 'Best Practices',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
create: function (context) {
|
||||
function checkClassName(node) {
|
||||
if (node.id && node.id.name && !node.id.name.startsWith('Umb')) {
|
||||
context.report({
|
||||
node: node.id,
|
||||
message: 'Class declaration should be prefixed with "Umb"',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ClassDeclaration: checkClassName,
|
||||
};
|
||||
},
|
||||
};
|
||||
49
src/Umbraco.Web.UI.Client/devops/example-runner/index.js
Normal file
49
src/Umbraco.Web.UI.Client/devops/example-runner/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as readline from 'readline';
|
||||
import { execSync } from 'child_process';
|
||||
import { readdir } from 'fs/promises';
|
||||
|
||||
const exampleDirectory = 'examples';
|
||||
|
||||
const getDirectories = async (source) =>
|
||||
(await readdir(source, { withFileTypes: true }))
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name)
|
||||
|
||||
async function pickExampleUI(){
|
||||
|
||||
// Find sub folder:
|
||||
const exampleFolderNames = await getDirectories(`${exampleDirectory}`);
|
||||
|
||||
// Create UI:
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
// List examples:
|
||||
console.log('Please select an example by entering the corresponding number:');
|
||||
exampleFolderNames.forEach((folder, index) => {
|
||||
console.log(`[${index + 1}] ${folder}`);
|
||||
});
|
||||
|
||||
// Ask user to select an example:
|
||||
rl.question('Enter your selection: ', (answer) => {
|
||||
|
||||
// User picked an example:
|
||||
const selectedFolder = exampleFolderNames[parseInt(answer) - 1];
|
||||
console.log(`You selected: ${selectedFolder}`);
|
||||
|
||||
process.env['VITE_EXAMPLE_PATH'] = `${exampleDirectory}/${selectedFolder}`;
|
||||
|
||||
// Start vite server:
|
||||
try {
|
||||
execSync('npm run dev', {stdio: 'inherit'});
|
||||
} catch (error) {
|
||||
// Nothing, cause this is most likely just the server begin stopped.
|
||||
//console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
pickExampleUI();
|
||||
203
src/Umbraco.Web.UI.Client/devops/icons/index.js
Normal file
203
src/Umbraco.Web.UI.Client/devops/icons/index.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import { readFileSync, writeFile, mkdir, rmSync } from 'fs';
|
||||
import * as globModule from 'tiny-glob';
|
||||
import * as pathModule from 'path';
|
||||
|
||||
const path = pathModule.default;
|
||||
const getDirName = path.dirname;
|
||||
const glob = globModule.default;
|
||||
|
||||
const moduleDirectory = 'src/packages/core/icon-registry';
|
||||
const iconsOutputDirectory = `${moduleDirectory}/icons`;
|
||||
const umbracoSvgDirectory = `${moduleDirectory}/svgs`;
|
||||
const iconMapJson = `${moduleDirectory}/icon-dictionary.json`;
|
||||
|
||||
const lucideSvgDirectory = 'node_modules/lucide-static/icons';
|
||||
const simpleIconsSvgDirectory = 'node_modules/simple-icons/icons';
|
||||
|
||||
const run = async () => {
|
||||
// Empty output directory:
|
||||
rmSync(iconsOutputDirectory, { recursive: true });
|
||||
|
||||
var icons = await collectDictionaryIcons();
|
||||
icons = await collectDiskIcons(icons);
|
||||
writeIconsToDisk(icons);
|
||||
generateJS(icons);
|
||||
};
|
||||
|
||||
const collectDictionaryIcons = async () => {
|
||||
const rawData = readFileSync(iconMapJson);
|
||||
const fileRaw = rawData.toString();
|
||||
const fileJSON = JSON.parse(fileRaw);
|
||||
|
||||
let icons = [];
|
||||
|
||||
// Lucide:
|
||||
fileJSON.lucide.forEach((iconDef) => {
|
||||
if (iconDef.file && iconDef.name) {
|
||||
const path = lucideSvgDirectory + '/' + iconDef.file;
|
||||
|
||||
try {
|
||||
const rawData = readFileSync(path);
|
||||
// For Lucide icons specially we adjust the icons a bit for them to work in our case: [NL]
|
||||
let svg = rawData.toString().replace(' width="24"\n', '');
|
||||
svg = svg.replace(' height="24"\n', '');
|
||||
svg = svg.replace('stroke-width="2"', 'stroke-width="1.75"');
|
||||
const iconFileName = iconDef.name;
|
||||
|
||||
const icon = {
|
||||
name: iconDef.name,
|
||||
legacy: iconDef.legacy,
|
||||
fileName: iconFileName,
|
||||
svg,
|
||||
output: `${iconsOutputDirectory}/${iconFileName}.ts`,
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
} catch (e) {
|
||||
console.log(`[Lucide] Could not load file: '${path}'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// SimpleIcons:
|
||||
fileJSON.simpleIcons.forEach((iconDef) => {
|
||||
if (iconDef.file && iconDef.name) {
|
||||
const path = simpleIconsSvgDirectory + '/' + iconDef.file;
|
||||
|
||||
try {
|
||||
const rawData = readFileSync(path);
|
||||
let svg = rawData.toString();
|
||||
const iconFileName = iconDef.name;
|
||||
|
||||
// SimpleIcons need to use fill="currentColor"
|
||||
const pattern = /fill=/g;
|
||||
if (!pattern.test(svg)) {
|
||||
svg = svg.replace(/<path/g, '<path fill="currentColor"');
|
||||
}
|
||||
|
||||
const icon = {
|
||||
name: iconDef.name,
|
||||
legacy: iconDef.legacy,
|
||||
fileName: iconFileName,
|
||||
svg,
|
||||
output: `${iconsOutputDirectory}/${iconFileName}.ts`,
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
} catch (e) {
|
||||
console.log(`[SimpleIcons] Could not load file: '${path}'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Umbraco:
|
||||
fileJSON.umbraco.forEach((iconDef) => {
|
||||
if (iconDef.file && iconDef.name) {
|
||||
const path = umbracoSvgDirectory + '/' + iconDef.file;
|
||||
|
||||
try {
|
||||
const rawData = readFileSync(path);
|
||||
const svg = rawData.toString();
|
||||
const iconFileName = iconDef.name;
|
||||
|
||||
const icon = {
|
||||
name: iconDef.name,
|
||||
legacy: iconDef.legacy,
|
||||
fileName: iconFileName,
|
||||
svg,
|
||||
output: `${iconsOutputDirectory}/${iconFileName}.ts`,
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
} catch (e) {
|
||||
console.log(`[Umbraco] Could not load file: '${path}'`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return icons;
|
||||
};
|
||||
|
||||
const collectDiskIcons = async (icons) => {
|
||||
const iconPaths = await glob(`${umbracoSvgDirectory}/icon-*.svg`);
|
||||
|
||||
iconPaths.forEach((path) => {
|
||||
const rawData = readFileSync(path);
|
||||
const svg = rawData.toString();
|
||||
const parsed = pathModule.parse(path);
|
||||
|
||||
if (!parsed) {
|
||||
console.log('No match found for: ', path);
|
||||
return;
|
||||
}
|
||||
|
||||
const SVGFileName = parsed.name;
|
||||
const iconFileName = SVGFileName.replace('.svg', '');
|
||||
const iconName = iconFileName;
|
||||
|
||||
// Only append not already defined icons:
|
||||
if (!icons.find((x) => x.name === iconName)) {
|
||||
const icon = {
|
||||
name: iconName,
|
||||
legacy: true,
|
||||
fileName: iconFileName,
|
||||
svg,
|
||||
output: `${iconsOutputDirectory}/${iconFileName}.ts`,
|
||||
};
|
||||
|
||||
icons.push(icon);
|
||||
}
|
||||
});
|
||||
|
||||
return icons;
|
||||
};
|
||||
|
||||
const writeIconsToDisk = (icons) => {
|
||||
icons.forEach((icon) => {
|
||||
const content = 'export default `' + icon.svg + '`;';
|
||||
|
||||
writeFileWithDir(icon.output, content, (err) => {
|
||||
if (err) {
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
//console.log(`icon: ${icon.name} generated`);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const generateJS = (icons) => {
|
||||
const JSPath = `${moduleDirectory}/icons.ts`;
|
||||
|
||||
const iconDescriptors = icons.map((icon) => {
|
||||
return `{
|
||||
name: "${icon.name}",
|
||||
${icon.legacy ? 'legacy: true,' : ''}
|
||||
path: () => import("./icons/${icon.fileName}.js"),
|
||||
}`.replace(/\t/g, ''); // Regex removes white space [NL]
|
||||
});
|
||||
|
||||
const content = `export default [${iconDescriptors.join(',')}];`;
|
||||
|
||||
writeFileWithDir(JSPath, content, (err) => {
|
||||
if (err) {
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
console.log('Icons outputted and Icon Manifests generated!');
|
||||
});
|
||||
};
|
||||
|
||||
const writeFileWithDir = (path, contents, cb) => {
|
||||
mkdir(getDirName(path), { recursive: true }, function (err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
writeFile(path, contents, cb);
|
||||
});
|
||||
};
|
||||
|
||||
run();
|
||||
File diff suppressed because it is too large
Load Diff
28
src/Umbraco.Web.UI.Client/devops/importmap/index.js
Normal file
28
src/Umbraco.Web.UI.Client/devops/importmap/index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { packageJsonExports, packageJsonName } from '../package/index.js';
|
||||
|
||||
export const createImportMap = (args) => {
|
||||
const imports = {
|
||||
...args.additionalImports,
|
||||
};
|
||||
|
||||
// Iterate over the exports in package.json
|
||||
for (const [key, value] of Object.entries(packageJsonExports || {})) {
|
||||
// remove leading ./
|
||||
if (value && value.endsWith('.js')) {
|
||||
const moduleName = key.replace(/^\.\//, '');
|
||||
|
||||
// replace ./dist-cms with src and remove /index.js
|
||||
let modulePath = value;
|
||||
if (typeof args.rootDir !== 'undefined') modulePath = modulePath.replace(/^\.\/dist-cms/, args.rootDir);
|
||||
if (args.replaceModuleExtensions) modulePath = modulePath.replace('.js', '.ts');
|
||||
console.log('replacing', value, 'with', modulePath);
|
||||
const importAlias = `${packageJsonName}/${moduleName}`;
|
||||
|
||||
imports[importAlias] = modulePath;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
imports,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { writeFileSync } from 'fs';
|
||||
import { createImportMap } from '../importmap/index.js';
|
||||
|
||||
const tsPath = './src/json-schema/all-packages.ts';
|
||||
|
||||
const importmap = createImportMap({
|
||||
rootDir: './src',
|
||||
replaceModuleExtensions: true,
|
||||
});
|
||||
|
||||
const paths = Object.keys(importmap.imports);
|
||||
|
||||
const content = `
|
||||
${
|
||||
paths.map(path => `import '${path}';`).join('\n')
|
||||
}
|
||||
`;
|
||||
|
||||
//const config = await resolveConfig('./.prettierrc.json');
|
||||
//const formattedContent = await format(content, { ...config, parser: 'json' });
|
||||
|
||||
writeFileSync(tsPath, content);
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* This script is used to compare the keys in the localization files. It will take a main language (en.js) and compare with the other languages.
|
||||
* The script will output the keys that are missing in the other languages and the keys that are missing in the main language.
|
||||
*
|
||||
* Note: Since the source files are TypeScript files, the script will only compare on the dist-cms files.
|
||||
*
|
||||
* Usage: node devops/localization/compare-languages.js [filter]
|
||||
* Example: node devops/localization/compare-languages.js da-dk.js
|
||||
*
|
||||
* Copyright (c) 2024 by Umbraco HQ
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const mainLanguage = 'en.js';
|
||||
const __dirname = import.meta.dirname;
|
||||
const languageFolder = path.join(__dirname, '../../dist-cms/assets/lang');
|
||||
|
||||
// Check that the languageFolder exists
|
||||
if (!fs.existsSync(languageFolder)) {
|
||||
console.error(`The language folder does not exist: ${languageFolder}. You need to build the project first by running 'npm run build'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mainKeys = (await import(path.join(languageFolder, mainLanguage))).default;
|
||||
const mainMap = buildMap(mainKeys);
|
||||
|
||||
const filter = process.argv[2];
|
||||
if (filter) {
|
||||
console.log(`Filtering on: ${filter}`);
|
||||
}
|
||||
|
||||
const languages = fs.readdirSync(languageFolder).filter((file) => file !== mainLanguage && file.endsWith('.js') && (!filter || file.includes(filter)));
|
||||
const missingKeysInMain = [];
|
||||
|
||||
const languagePromise = Promise.all(languages.map(async (language) => {
|
||||
const languageKeys = (await import(path.join(languageFolder, language))).default;
|
||||
const languageMap = buildMap(languageKeys);
|
||||
|
||||
const missingKeys = Array.from(mainMap.keys()).filter((key) => !languageMap.has(key));
|
||||
let localMissingKeysInMain = Array.from(languageMap.keys()).filter((key) => !mainMap.has(key));
|
||||
localMissingKeysInMain = localMissingKeysInMain.map((key) => `${key} (${language})`);
|
||||
missingKeysInMain.push(...localMissingKeysInMain);
|
||||
|
||||
console.log(`\n${language}:`);
|
||||
console.log(`Missing keys in ${language}:`);
|
||||
console.log(missingKeys);
|
||||
}));
|
||||
|
||||
await languagePromise;
|
||||
|
||||
console.log(`Missing keys in ${mainLanguage}:`);
|
||||
console.log(missingKeysInMain);
|
||||
|
||||
function buildMap(keys) {
|
||||
const map = new Map();
|
||||
|
||||
for (const key in keys) {
|
||||
for (const subKey in keys[key]) {
|
||||
map.set(`${key}_${subKey}`, keys[key][subKey]);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* This script is used to find unused language keys in the javascript files. It will take a main language (en.js) and compare with the other languages.
|
||||
*
|
||||
* Usage: node devops/localization/unused-language-keys.js
|
||||
* Example: node devops/localization/unused-language-keys.js
|
||||
*
|
||||
* Copyright (c) 2024 by Umbraco HQ
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import glob from 'tiny-glob';
|
||||
|
||||
const mainLanguage = 'en.js';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
const languageFolder = path.join(__dirname, '../../dist-cms/assets/lang');
|
||||
|
||||
// Check that the languageFolder exists
|
||||
if (!fs.existsSync(languageFolder)) {
|
||||
console.error(`The language folder does not exist: ${languageFolder}. You need to build the project first by running 'npm run build'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mainKeys = (await import(path.join(languageFolder, mainLanguage))).default;
|
||||
const mainMap = buildMap(mainKeys);
|
||||
const keys = Array.from(mainMap.keys());
|
||||
const usedKeys = new Set();
|
||||
|
||||
const elementAndControllerFiles = await glob(`${__dirname}/../../src/**/*.ts`, { filesOnly: true });
|
||||
|
||||
console.log(`Checking ${elementAndControllerFiles.length} files for unused keys`);
|
||||
|
||||
// Find all the keys used in the javascript files
|
||||
const filePromise = Promise.all(elementAndControllerFiles.map(async (file) => {
|
||||
// Check if each key is in the file (simple)
|
||||
const fileContent = fs.readFileSync(file, 'utf8');
|
||||
keys.forEach((key) => {
|
||||
if (fileContent.includes(key)) {
|
||||
usedKeys.add(key);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
await filePromise;
|
||||
|
||||
const unusedKeys = Array.from(mainMap.keys()).filter((key) => !usedKeys.has(key));
|
||||
|
||||
console.log(`\n${mainLanguage}:`);
|
||||
console.log(`Used keys in ${mainLanguage}:`);
|
||||
console.log(usedKeys);
|
||||
console.log(`Unused keys in ${mainLanguage}:`);
|
||||
console.log(unusedKeys);
|
||||
|
||||
function buildMap(keys) {
|
||||
const map = new Map();
|
||||
|
||||
for (const key in keys) {
|
||||
for (const subKey in keys[key]) {
|
||||
map.set(`${key}_${subKey}`, keys[key][subKey]);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from '@hey-api/openapi-ts';
|
||||
|
||||
export default defineConfig({
|
||||
client: 'fetch',
|
||||
input: 'https://raw.githubusercontent.com/umbraco/Umbraco-CMS/v15/dev/src/Umbraco.Cms.Api.Management/OpenApi.json',
|
||||
output: {
|
||||
path: 'src/external/backend-api/src',
|
||||
format: 'prettier',
|
||||
lint: 'eslint',
|
||||
},
|
||||
schemas: false,
|
||||
services: {
|
||||
asClass: true,
|
||||
},
|
||||
types: {
|
||||
enums: 'typescript',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from '@hey-api/openapi-ts';
|
||||
|
||||
export default defineConfig({
|
||||
client: 'fetch',
|
||||
debug: true,
|
||||
input: 'http://localhost:11000/umbraco/swagger/management/swagger.json',
|
||||
output: {
|
||||
path: 'src/external/backend-api/src',
|
||||
format: 'prettier',
|
||||
lint: 'eslint',
|
||||
},
|
||||
schemas: false,
|
||||
services: {
|
||||
asClass: true,
|
||||
},
|
||||
types: {
|
||||
enums: 'typescript',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from '@hey-api/openapi-ts';
|
||||
|
||||
export default defineConfig({
|
||||
client: 'fetch',
|
||||
debug: true,
|
||||
input: '../Umbraco.Cms.Api.Management/OpenApi.json',
|
||||
output: {
|
||||
path: 'src/external/backend-api/src',
|
||||
format: 'prettier',
|
||||
lint: 'eslint',
|
||||
},
|
||||
schemas: false,
|
||||
services: {
|
||||
asClass: true,
|
||||
},
|
||||
types: {
|
||||
enums: 'typescript',
|
||||
},
|
||||
});
|
||||
1
src/Umbraco.Web.UI.Client/devops/package/index.js
Normal file
1
src/Umbraco.Web.UI.Client/devops/package/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from './meta.js';
|
||||
7
src/Umbraco.Web.UI.Client/devops/package/meta.js
Normal file
7
src/Umbraco.Web.UI.Client/devops/package/meta.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
export const packageJsonPath = 'package.json';
|
||||
export const packageJsonData = JSON.parse(readFileSync(packageJsonPath).toString());
|
||||
export const packageJsonName = packageJsonData.name;
|
||||
export const packageJsonVersion = packageJsonData.version;
|
||||
export const packageJsonExports = packageJsonData.exports;
|
||||
26
src/Umbraco.Web.UI.Client/devops/package/validate-exports.js
Normal file
26
src/Umbraco.Web.UI.Client/devops/package/validate-exports.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { globSync } from 'glob';
|
||||
import { packageJsonExports } from './meta.js';
|
||||
|
||||
const validateExports = async () => {
|
||||
const errors = [];
|
||||
|
||||
// Iterate over the exports in package.json
|
||||
for (const [key, value] of Object.entries(packageJsonExports || {})) {
|
||||
if (value) {
|
||||
const jsFiles = await globSync(value);
|
||||
|
||||
// Log an error if the export from the package.json does not exist in the build output
|
||||
if (jsFiles.length === 0) {
|
||||
errors.push(`Could not find export: ${key} -> ${value} in the build output.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join('\n'));
|
||||
} else {
|
||||
console.log('--- Exports validated successfully. ---');
|
||||
}
|
||||
};
|
||||
|
||||
validateExports();
|
||||
16
src/Umbraco.Web.UI.Client/devops/publish/cleanse-pkg.js
Normal file
16
src/Umbraco.Web.UI.Client/devops/publish/cleanse-pkg.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
console.log('[Prepublish] Cleansing package.json');
|
||||
|
||||
const packageFile = './package.json';
|
||||
const packageJson = JSON.parse(readFileSync(packageFile, 'utf8'));
|
||||
|
||||
// Remove all DevDependencies
|
||||
delete packageJson.devDependencies;
|
||||
|
||||
// Rename dependencies to peerDependencies
|
||||
packageJson.peerDependencies = { ...packageJson.dependencies };
|
||||
delete packageJson.dependencies;
|
||||
|
||||
// Write the package.json back to disk
|
||||
writeFileSync(packageFile, JSON.stringify(packageJson, null, 2), 'utf8');
|
||||
111
src/Umbraco.Web.UI.Client/devops/tsc-override/index.js
Normal file
111
src/Umbraco.Web.UI.Client/devops/tsc-override/index.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// Notice: This script is not perfect and may not work in all cases. ex. it places the override term wrong for async and setter/getter methods. But its a help any way. [NL]
|
||||
|
||||
import ts from 'typescript';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const tsconfigPath = './tsconfig.json';
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
async function fixOverride() {
|
||||
const configFile = path.isAbsolute(tsconfigPath)
|
||||
? tsconfigPath
|
||||
: ts.findConfigFile(cwd, ts.sys.fileExists, tsconfigPath);
|
||||
|
||||
if (!configFile) {
|
||||
console.error('No tsconfig file found for path:', tsconfigPath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config = ts.readConfigFile(configFile, ts.sys.readFile);
|
||||
|
||||
const { options, fileNames } = ts.parseJsonConfigFileContent(
|
||||
config.config,
|
||||
ts.sys,
|
||||
|
||||
// Resolve to the folder where the tsconfig file located
|
||||
path.dirname(tsconfigPath),
|
||||
);
|
||||
|
||||
const program = ts.createProgram({
|
||||
rootNames: fileNames,
|
||||
options,
|
||||
});
|
||||
|
||||
if (fileNames.length === 0) {
|
||||
console.error('No files in the project.', {
|
||||
fileNames,
|
||||
options,
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
let emitResult = program.emit();
|
||||
|
||||
const overrideErrors = ts
|
||||
.getPreEmitDiagnostics(program)
|
||||
.concat(emitResult.diagnostics)
|
||||
.filter((diagnostic) =>
|
||||
[
|
||||
// This member must have an 'override' modifier because it overrides a member in the base class '{0}'.
|
||||
4114,
|
||||
// This parameter property must have an 'override' modifier because it overrides a member in base class '{0}'
|
||||
4115,
|
||||
// This member must have an 'override' modifier because it overrides an abstract method that is declared in the base class '{0}'.
|
||||
4116,
|
||||
].includes(diagnostic.code),
|
||||
);
|
||||
|
||||
const sortedErrors = sortErrors(overrideErrors);
|
||||
|
||||
for (const diagnostic of sortedErrors) {
|
||||
await addOverride(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ts.Diagnostic} diagnostic
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function addOverride(diagnostic) {
|
||||
const fileContent = (await fs.readFile(diagnostic.file.fileName, 'utf-8')).toString();
|
||||
let startIndex = diagnostic.start;
|
||||
|
||||
if (fileContent.slice(0, startIndex).endsWith(' get ')) {
|
||||
startIndex -= 'get '.length;
|
||||
}
|
||||
if (fileContent.slice(0, startIndex).endsWith(' async ')) {
|
||||
startIndex -= 'async '.length;
|
||||
}
|
||||
if (fileContent.slice(0, startIndex).endsWith(' readonly ')) {
|
||||
startIndex -= 'readonly '.length;
|
||||
}
|
||||
|
||||
const newFileContent = [fileContent.slice(0, startIndex), 'override ', fileContent.slice(startIndex)].join('');
|
||||
await fs.writeFile(diagnostic.file.fileName, newFileContent);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ts.Diagnostic[]} errors
|
||||
* @returns {ts.Diagnostic[]}
|
||||
*/
|
||||
function sortErrors(errors) {
|
||||
// Sort by file path and start position from end to start
|
||||
// so we can insert override keyword without changing the start position of other errors in the same file that happen before
|
||||
return errors.slice(0).sort((a, b) => {
|
||||
if (a.file && b.file) {
|
||||
if (a.file.fileName === b.file.fileName) {
|
||||
return b.start - a.start;
|
||||
}
|
||||
|
||||
return a.file.fileName.localeCompare(b.file.fileName);
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
fixOverride();
|
||||
74
src/Umbraco.Web.UI.Client/devops/tsconfig/index.js
Normal file
74
src/Umbraco.Web.UI.Client/devops/tsconfig/index.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { writeFileSync } from 'fs';
|
||||
import { format, resolveConfig } from 'prettier';
|
||||
import { createImportMap } from '../importmap/index.js';
|
||||
|
||||
const tsconfigPath = 'tsconfig.json';
|
||||
const tsconfigComment = `
|
||||
/* -------------------------------------------------------------------------
|
||||
|
||||
|
||||
DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
|
||||
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------- */
|
||||
`;
|
||||
|
||||
const tsConfigBase = {
|
||||
compilerOptions: {
|
||||
module: 'esnext',
|
||||
moduleResolution: 'bundler',
|
||||
moduleDetection: 'force',
|
||||
verbatimModuleSyntax: true,
|
||||
target: 'es2022',
|
||||
lib: ['es2022', 'dom', 'dom.iterable'],
|
||||
outDir: './types',
|
||||
allowSyntheticDefaultImports: true,
|
||||
experimentalDecorators: true,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
useDefineForClassFields: false,
|
||||
baseUrl: '.',
|
||||
incremental: true,
|
||||
skipLibCheck: true,
|
||||
noImplicitOverride: true,
|
||||
allowImportingTsExtensions: true,
|
||||
resolveJsonModule: true,
|
||||
isolatedModules: true,
|
||||
noEmit: true,
|
||||
/* Linting */
|
||||
strict: true,
|
||||
noFallthroughCasesInSwitch: true,
|
||||
noImplicitReturns: true,
|
||||
},
|
||||
include: ['src/**/*.ts', 'apps/**/*.ts', 'e2e/**/*.ts', 'index.ts', 'storybook/stories/**/*.ts', 'examples/**/*.ts'],
|
||||
references: [
|
||||
{
|
||||
path: './tsconfig.node.json',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const importmap = createImportMap({
|
||||
rootDir: './src',
|
||||
additionalImports: {
|
||||
'@umbraco-cms/internal/test-utils': './utils/test-utils.ts',
|
||||
},
|
||||
replaceModuleExtensions: true,
|
||||
});
|
||||
|
||||
const paths = {};
|
||||
|
||||
for (const [key, value] of Object.entries(importmap.imports)) {
|
||||
const valueAsArray = [value];
|
||||
paths[key] = valueAsArray;
|
||||
}
|
||||
|
||||
tsConfigBase.compilerOptions.paths = paths;
|
||||
|
||||
const content = tsconfigComment + JSON.stringify(tsConfigBase, null, ' ');
|
||||
|
||||
const config = await resolveConfig('./.prettierrc.json');
|
||||
const formattedContent = await format(content, { ...config, parser: 'json' });
|
||||
|
||||
writeFileSync(tsconfigPath, formattedContent);
|
||||
13
src/Umbraco.Web.UI.Client/devops/utils/index.js
Normal file
13
src/Umbraco.Web.UI.Client/devops/utils/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { writeFile, mkdir } from 'fs';
|
||||
import * as pathModule from 'path';
|
||||
|
||||
const path = pathModule.default;
|
||||
const getDirName = path.dirname;
|
||||
|
||||
export const writeFileWithDir = (path, contents, cb) => {
|
||||
mkdir(getDirName(path), { recursive: true }, function (err) {
|
||||
if (err) return cb(err);
|
||||
|
||||
writeFile(path, contents, cb);
|
||||
});
|
||||
};
|
||||
95
src/Umbraco.Web.UI.Client/e2e/installer.spec.ts
Normal file
95
src/Umbraco.Web.UI.Client/e2e/installer.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
const { rest } = window.MockServiceWorker;
|
||||
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
import {
|
||||
type ProblemDetails,
|
||||
RuntimeLevelModel,
|
||||
type ServerStatusResponseModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { expect, test } from './test.js';
|
||||
|
||||
test.describe('installer tests', () => {
|
||||
test.beforeEach(async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<ServerStatusResponseModel>({
|
||||
serverStatus: RuntimeLevelModel.INSTALL,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto('/install');
|
||||
|
||||
await page.waitForSelector('[data-test="installer"]');
|
||||
});
|
||||
|
||||
test('installer is shown', async ({ page }) => {
|
||||
await expect(page).toHaveURL('/install');
|
||||
});
|
||||
|
||||
test.describe('test success and failure', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.waitForSelector('[data-test="installer-user"]');
|
||||
await page.fill('[aria-label="name"]', 'Test');
|
||||
await page.fill('[aria-label="email"]', 'test@umbraco');
|
||||
await page.fill('[aria-label="password"]', 'test123456');
|
||||
await page.click('[name="subscribeToNewsletter"]');
|
||||
|
||||
// Go to the next step
|
||||
await page.click('[aria-label="Next"]');
|
||||
|
||||
// Set telemetry
|
||||
await page.waitForSelector('[data-test="installer-telemetry"]');
|
||||
await page.waitForSelector('uui-slider[name="telemetryLevel"]');
|
||||
|
||||
// Click [aria-label="Next"]
|
||||
await page.click('[aria-label="Next"]');
|
||||
|
||||
// Database form
|
||||
await page.waitForSelector('[data-test="installer-database"]');
|
||||
});
|
||||
|
||||
test('installer completes successfully', async ({ page }) => {
|
||||
await page.click('[aria-label="Install"]');
|
||||
await page.waitForSelector('umb-backoffice', { timeout: 30000 });
|
||||
});
|
||||
|
||||
test('installer fails', async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.post(umbracoPath('/install/setup'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetails>({
|
||||
status: 400,
|
||||
type: 'validation',
|
||||
detail: 'Something went wrong',
|
||||
errors: {
|
||||
databaseName: ['The database name is required'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await page.click('[aria-label="Install"]');
|
||||
|
||||
await page.waitForSelector('[data-test="installer-error"]');
|
||||
|
||||
await expect(page.locator('[data-test="error-message"]')).toHaveText('Something went wrong', {
|
||||
useInnerText: true,
|
||||
});
|
||||
|
||||
// Click reset button
|
||||
await page.click('#button-reset');
|
||||
|
||||
await page.waitForSelector('[data-test="installer-user"]');
|
||||
});
|
||||
});
|
||||
});
|
||||
4
src/Umbraco.Web.UI.Client/e2e/package.json
Normal file
4
src/Umbraco.Web.UI.Client/e2e/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "backoffice-e2e",
|
||||
"type": "commonjs"
|
||||
}
|
||||
20
src/Umbraco.Web.UI.Client/e2e/test.ts
Normal file
20
src/Umbraco.Web.UI.Client/e2e/test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { expect, test as base } from '@playwright/test';
|
||||
import { createWorkerFixture } from 'playwright-msw';
|
||||
import type { MockServiceWorker } from 'playwright-msw';
|
||||
|
||||
import { handlers } from '../src/mocks/e2e-handlers.js';
|
||||
|
||||
const test = base.extend<{
|
||||
worker: MockServiceWorker;
|
||||
}>({
|
||||
worker: createWorkerFixture(handlers),
|
||||
page: async ({ page }, use) => {
|
||||
// Set is-authenticated in sessionStorage to true
|
||||
await page.addInitScript(`window.sessionStorage.setItem('is-authenticated', 'true');`);
|
||||
|
||||
// Use signed-in page in all tests
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
export { test, expect };
|
||||
65
src/Umbraco.Web.UI.Client/e2e/upgrader.spec.ts
Normal file
65
src/Umbraco.Web.UI.Client/e2e/upgrader.spec.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
const { rest } = window.MockServiceWorker;
|
||||
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
|
||||
import {
|
||||
type ProblemDetails,
|
||||
RuntimeLevelModel,
|
||||
type ServerStatusResponseModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { expect, test } from './test.js';
|
||||
|
||||
test.describe('upgrader tests', () => {
|
||||
test.beforeEach(async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<ServerStatusResponseModel>({
|
||||
serverStatus: RuntimeLevelModel.UPGRADE,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await page.goto('/upgrade');
|
||||
});
|
||||
|
||||
test('upgrader is shown', async ({ page }) => {
|
||||
await page.waitForSelector('[data-test="upgrader"]');
|
||||
await expect(page).toHaveURL('/upgrade');
|
||||
await expect(page.locator('h1')).toHaveText('Upgrading Umbraco', { useInnerText: true });
|
||||
});
|
||||
|
||||
test('upgrader has a "View Report" button', async ({ page }) => {
|
||||
await expect(page.locator('[data-test="view-report-button"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('upgrader completes successfully', async ({ page }) => {
|
||||
await page.click('[data-test="continue-button"]');
|
||||
await page.waitForSelector('umb-backoffice', { timeout: 30000 });
|
||||
});
|
||||
|
||||
test('upgrader fails and shows error', async ({ page, worker }) => {
|
||||
await worker.use(
|
||||
// Override the server status to be "must-install"
|
||||
rest.post(umbracoPath('/upgrade/authorize'), (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetails>({
|
||||
status: 400,
|
||||
type: 'error',
|
||||
detail: 'Something went wrong',
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await page.click('[data-test="continue-button"]');
|
||||
|
||||
await expect(page.locator('[data-test="error-message"]')).toHaveText('Something went wrong', {
|
||||
useInnerText: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
27
src/Umbraco.Web.UI.Client/eslint-local-rules.cjs
Normal file
27
src/Umbraco.Web.UI.Client/eslint-local-rules.cjs
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const badTypeImportRule = require('./devops/eslint/rules/bad-type-import.cjs');
|
||||
const enforceElementSuffixOnElementClassNameRule = require('./devops/eslint/rules/enforce-element-suffix-on-element-class-name.cjs');
|
||||
const enforceUmbPrefixOnElementNameRule = require('./devops/eslint/rules/enforce-umb-prefix-on-element-name.cjs');
|
||||
const enforceUmbracoExternalImportsRule = require('./devops/eslint/rules/enforce-umbraco-external-imports.cjs');
|
||||
const ensureRelativeImportUseJsExtensionRule = require('./devops/eslint/rules/ensure-relative-import-use-js-extension.cjs');
|
||||
const exportedStringConstantNaming = require('./devops/eslint/rules/exported-string-constant-naming.cjs');
|
||||
const noDirectApiImportRule = require('./devops/eslint/rules/no-direct-api-import.cjs');
|
||||
const preferImportAliasesRule = require('./devops/eslint/rules/prefer-import-aliases.cjs');
|
||||
const preferStaticStylesLastRule = require('./devops/eslint/rules/prefer-static-styles-last.cjs');
|
||||
const umbClassPrefixRule = require('./devops/eslint/rules/umb-class-prefix.cjs');
|
||||
const noRelativeImportToImportMapModule = require('./devops/eslint/rules/no-relative-import-to-import-map-module.cjs');
|
||||
|
||||
module.exports = {
|
||||
'bad-type-import': badTypeImportRule,
|
||||
'enforce-element-suffix-on-element-class-name': enforceElementSuffixOnElementClassNameRule,
|
||||
'enforce-umb-prefix-on-element-name': enforceUmbPrefixOnElementNameRule,
|
||||
'enforce-umbraco-external-imports': enforceUmbracoExternalImportsRule,
|
||||
'ensure-relative-import-use-js-extension': ensureRelativeImportUseJsExtensionRule,
|
||||
'exported-string-constant-naming': exportedStringConstantNaming,
|
||||
'no-direct-api-import': noDirectApiImportRule,
|
||||
'prefer-import-aliases': preferImportAliasesRule,
|
||||
'prefer-static-styles-last': preferStaticStylesLastRule,
|
||||
'umb-class-prefix': umbClassPrefixRule,
|
||||
'no-relative-import-to-import-map-module': noRelativeImportToImportMapModule,
|
||||
};
|
||||
94
src/Umbraco.Web.UI.Client/eslint.config.js
Normal file
94
src/Umbraco.Web.UI.Client/eslint.config.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import localRules from 'eslint-plugin-local-rules';
|
||||
import wcPlugin from 'eslint-plugin-wc';
|
||||
import litPlugin from 'eslint-plugin-lit';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import jsdoc from 'eslint-plugin-jsdoc';
|
||||
|
||||
export default [
|
||||
// Recommended config applied to all files
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
wcPlugin.configs['flat/recommended'],
|
||||
litPlugin.configs['flat/recommended'],
|
||||
jsdoc.configs['flat/recommended'], // We use the non typescript version to allow types to be defined in the jsdoc comments. This will allow js docs as an alternative to typescript types.
|
||||
localRules.configs.all,
|
||||
eslintPluginPrettierRecommended,
|
||||
|
||||
// Global ignores
|
||||
{
|
||||
ignores: [
|
||||
'**/eslint.config.js',
|
||||
'**/rollup.config.js',
|
||||
'**/vite.config.ts',
|
||||
'src/external',
|
||||
'src/packages/core/icon-registry/icons',
|
||||
'src/packages/core/icon-registry/icons.ts',
|
||||
'src/**/*.test.ts',
|
||||
],
|
||||
},
|
||||
|
||||
// Global config
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
import: importPlugin,
|
||||
'local-rules': localRules,
|
||||
},
|
||||
rules: {
|
||||
semi: ['warn', 'always'],
|
||||
'prettier/prettier': ['warn', { endOfLine: 'auto' }],
|
||||
'no-unused-vars': 'off', //Let '@typescript-eslint/no-unused-vars' catch the errors to allow unused function parameters (ex: in interfaces)
|
||||
'no-var': 'error',
|
||||
...importPlugin.configs.recommended.rules,
|
||||
'import/namespace': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'import/order': ['warn', { groups: ['builtin', 'parent', 'sibling', 'index', 'external'] }],
|
||||
'import/no-self-import': 'error',
|
||||
'import/no-cycle': ['error', { maxDepth: 6, allowUnsafeDynamicCyclicDependency: true }],
|
||||
'import/no-named-as-default': 'off', // Does not work with eslint 9
|
||||
'import/no-named-as-default-member': 'off', // Does not work with eslint 9
|
||||
'local-rules/prefer-static-styles-last': 'warn',
|
||||
'local-rules/enforce-umbraco-external-imports': [
|
||||
'error',
|
||||
{
|
||||
exceptions: ['@umbraco-cms', '@open-wc/testing', '@storybook', 'msw', '.', 'vite'],
|
||||
},
|
||||
],
|
||||
'local-rules/exported-string-constant-naming': [
|
||||
'error',
|
||||
{
|
||||
excludedFileNames: ['umbraco-package', 'input-tiny-mce.defaults'], // TODO: what to do about the tiny mce defaults?
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'@typescript-eslint/consistent-type-exports': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/no-import-type-side-effects': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// Pattern-specific overrides
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
...tseslint.configs.disableTypeChecked,
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
7
src/Umbraco.Web.UI.Client/examples/README.md
Normal file
7
src/Umbraco.Web.UI.Client/examples/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Backoffice Examples
|
||||
|
||||
This folder contains example packages showcasing the usage of extensions in Backoffice.
|
||||
|
||||
The purpose of these projects includes serving as demonstration or example for
|
||||
packages, as well as testing to make sure the extension points continue
|
||||
to work in these situations and to assist in developing new integrations.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Property Dataset Dashboard Example
|
||||
|
||||
This example is a work in progress example of how to write a property editor.
|
||||
|
||||
This example covers a few points:
|
||||
|
||||
- Using an existing Property Editor Schema
|
||||
@@ -0,0 +1,57 @@
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { html, customElement, LitElement, property, css } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
|
||||
import type { UmbBlockDataType } from '@umbraco-cms/backoffice/block';
|
||||
import type { UmbBlockEditorCustomViewElement } from '@umbraco-cms/backoffice/block-custom-view';
|
||||
|
||||
// eslint-disable-next-line local-rules/enforce-umb-prefix-on-element-name
|
||||
@customElement('example-block-custom-view')
|
||||
// eslint-disable-next-line local-rules/umb-class-prefix
|
||||
export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implements UmbBlockEditorCustomViewElement {
|
||||
//
|
||||
@property({ attribute: false })
|
||||
content?: UmbBlockDataType;
|
||||
|
||||
@property({ attribute: false })
|
||||
settings?: UmbBlockDataType;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="uui-text ${this.settings?.blockAlignment ? 'align-' + this.settings?.blockAlignment : undefined}">
|
||||
<h5 class="uui-text">My Custom View</h5>
|
||||
<p>Headline: ${this.content?.headline}</p>
|
||||
<p>Alignment: ${this.settings?.blockAlignment}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: red;
|
||||
color: white;
|
||||
border-radius: 9px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default ExampleBlockCustomView;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'example-block-custom-view': ExampleBlockCustomView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export const manifests: Array<UmbExtensionManifest> = [
|
||||
{
|
||||
type: 'blockEditorCustomView',
|
||||
alias: 'Umb.blockEditorCustomView.TestView',
|
||||
name: 'Block Editor Custom View Test',
|
||||
element: () => import('./block-custom-view.js'),
|
||||
forContentTypeAlias: 'headlineUmbracoDemoBlock',
|
||||
forBlockEditor: ['block-list', 'block-grid'],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,46 @@
|
||||
import { EXAMPLE_MODAL_TOKEN, type ExampleModalData, type ExampleModalResult } from './example-modal-token.js';
|
||||
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
|
||||
import './example-custom-modal-element.element.js';
|
||||
|
||||
@customElement('example-custom-modal-dashboard')
|
||||
export class UmbExampleCustomModalDashboardElement extends UmbLitElement {
|
||||
|
||||
#modalManagerContext? : typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT,(instance)=>{
|
||||
this.#modalManagerContext = instance;
|
||||
})
|
||||
}
|
||||
|
||||
#onOpenModal(){
|
||||
this.#modalManagerContext?.open(this,EXAMPLE_MODAL_TOKEN,{})
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-box>
|
||||
<p>Open the custom modal</p>
|
||||
<uui-button look="primary" @click=${this.#onOpenModal}>Open Modal</uui-button>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [css`
|
||||
:host{
|
||||
display:block;
|
||||
padding:20px;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
||||
export default UmbExampleCustomModalDashboardElement
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'example-custom-modal-dashboard': UmbExampleCustomModalDashboardElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { css, html } from "@umbraco-cms/backoffice/external/lit";
|
||||
import { defineElement, UUIModalElement } from "@umbraco-cms/backoffice/external/uui";
|
||||
|
||||
/**
|
||||
* This class defines a custom design for the modal it self, in the same was as
|
||||
* UUIModalSidebarElement and UUIModalDialogElement.
|
||||
*/
|
||||
@defineElement('example-modal-element')
|
||||
export class UmbExampleCustomModalElement extends UUIModalElement {
|
||||
override render() {
|
||||
return html`
|
||||
<dialog>
|
||||
<h2>Custom Modal-wrapper</h2>
|
||||
<slot></slot>
|
||||
</dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
...UUIModalElement.styles,
|
||||
css`
|
||||
dialog {
|
||||
width:100%;
|
||||
height:100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
top:0;
|
||||
left:0;
|
||||
right:0;
|
||||
bottom:0;
|
||||
background:#fff;
|
||||
}
|
||||
:host([index='0']) dialog {
|
||||
box-shadow: var(--uui-shadow-depth-5);
|
||||
}
|
||||
:host(:not([index='0'])) dialog {
|
||||
outline: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbExampleCustomModalElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'example-modal-element': UmbExampleCustomModalElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { UmbModalToken } from "@umbraco-cms/backoffice/modal";
|
||||
|
||||
export interface ExampleModalData {
|
||||
unique: string | null;
|
||||
}
|
||||
|
||||
export interface ExampleModalResult {
|
||||
text : string;
|
||||
}
|
||||
|
||||
export const EXAMPLE_MODAL_TOKEN = new UmbModalToken<
|
||||
ExampleModalData,
|
||||
ExampleModalResult
|
||||
>('example.modal.custom.element', {
|
||||
modal : {
|
||||
type : 'custom',
|
||||
element: () => import('./example-custom-modal-element.element.js'),
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ExampleModalData, ExampleModalResult } from './example-modal-token.js';
|
||||
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import type { UmbModalContext } from '@umbraco-cms/backoffice/modal';
|
||||
import './example-custom-modal-element.element.js';
|
||||
|
||||
@customElement('example-modal-view')
|
||||
export class UmbExampleModalViewElement extends UmbLitElement {
|
||||
|
||||
@property({ attribute: false })
|
||||
public modalContext?: UmbModalContext<ExampleModalData, ExampleModalResult>;
|
||||
|
||||
onClickDone(){
|
||||
this.modalContext?.submit();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div id="modal">
|
||||
<p>Example content of custom modal element</p>
|
||||
<uui-button look="primary" label="Submit modal" @click=${() => this.onClickDone()}></uui-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [css`
|
||||
:host {
|
||||
background: #eaeaea;
|
||||
display: block;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
#modal {
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
p {
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
`];
|
||||
}
|
||||
|
||||
export default UmbExampleModalViewElement
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'example-modal-view': UmbExampleModalViewElement;
|
||||
}
|
||||
}
|
||||
29
src/Umbraco.Web.UI.Client/examples/custom-modal/index.ts
Normal file
29
src/Umbraco.Web.UI.Client/examples/custom-modal/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ManifestDashboard } from '@umbraco-cms/backoffice/dashboard';
|
||||
import type { ManifestModal } from '@umbraco-cms/backoffice/modal';
|
||||
|
||||
const demoModal : ManifestModal = {
|
||||
type: 'modal',
|
||||
name: 'Example Custom Modal Element',
|
||||
alias: 'example.modal.custom.element',
|
||||
js: () => import('./example-modal-view.element.js'),
|
||||
}
|
||||
|
||||
const demoModalsDashboard : ManifestDashboard = {
|
||||
type: 'dashboard',
|
||||
name: 'Example Custom Modal Dashboard',
|
||||
alias: 'example.dashboard.custom.modal.element',
|
||||
element: () => import('./example-custom-modal-dashboard.element.js'),
|
||||
weight: 900,
|
||||
meta: {
|
||||
label: 'Custom Modal',
|
||||
pathname: 'custom-modal',
|
||||
},
|
||||
conditions : [
|
||||
{
|
||||
alias: 'Umb.Condition.SectionAlias',
|
||||
match: 'Umb.Section.Content'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default [demoModal,demoModalsDashboard];
|
||||
@@ -0,0 +1,10 @@
|
||||
# Property Dataset Dashboard Example
|
||||
|
||||
This example demonstrates the essence of the Property Dataset.
|
||||
|
||||
This dashboard implements such, to display a few selected Property Editors and bind the data back to the Dashboard.
|
||||
|
||||
|
||||
## SVG code of Icons
|
||||
|
||||
Make sure to use currentColor for fill or stroke color, as that will make the icon adapt to the font color of where its begin used.
|
||||
@@ -0,0 +1,71 @@
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, LitElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
|
||||
import { type UmbPropertyValueData, type UmbPropertyDatasetElement } from '@umbraco-cms/backoffice/property';
|
||||
|
||||
@customElement('example-dataset-dashboard')
|
||||
export class ExampleDatasetDashboard extends UmbElementMixin(LitElement) {
|
||||
data: UmbPropertyValueData[] = [
|
||||
{
|
||||
alias: 'textProperty',
|
||||
value: 'Hello',
|
||||
},
|
||||
];
|
||||
|
||||
#onDataChange(e: Event) {
|
||||
const oldValue = this.data;
|
||||
this.data = (e.target as UmbPropertyDatasetElement).value;
|
||||
this.requestUpdate('data', oldValue);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-box class="uui-text">
|
||||
<h1 class="uui-h2" style="margin-top: var(--uui-size-layout-1);">Dataset Example</h1>
|
||||
<umb-property-dataset .value=${this.data} @change=${this.#onDataChange}>
|
||||
<umb-property
|
||||
label="Textual input"
|
||||
description="Example of text editor"
|
||||
alias="textProperty"
|
||||
property-editor-ui-alias="Umb.PropertyEditorUi.TextBox"></umb-property>
|
||||
<umb-property
|
||||
label="List of options"
|
||||
description="Example of dropdown editor"
|
||||
alias="listProperty"
|
||||
.config=${[
|
||||
{
|
||||
alias: 'multiple',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
alias: 'items',
|
||||
value: ['First Option', 'Second Option', 'Third Option'],
|
||||
},
|
||||
]}
|
||||
property-editor-ui-alias="Umb.PropertyEditorUi.Dropdown"></umb-property>
|
||||
</umb-property-dataset>
|
||||
|
||||
<h5 class="uui-h3" style="margin-top: var(--uui-size-layout-1);">Output of dashboard data:</h5>
|
||||
<code> ${JSON.stringify(this.data, null, 2)} </code>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: var(--uui-size-layout-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default ExampleDatasetDashboard;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'example-dataset-dashboard': ExampleDatasetDashboard;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ManifestDashboard } from '@umbraco-cms/backoffice/dashboard';
|
||||
|
||||
export const manifests: Array<ManifestDashboard> = [
|
||||
{
|
||||
type: 'dashboard',
|
||||
name: 'Example Dataset Dashboard',
|
||||
alias: 'example.dashboard.dataset',
|
||||
element: () => import('./dataset-dashboard.js'),
|
||||
weight: 900,
|
||||
meta: {
|
||||
label: 'Dataset example',
|
||||
pathname: 'dataset-example',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
const workspace: UmbExtensionManifest = {
|
||||
type: 'workspaceView',
|
||||
alias: 'Example.WorkspaceView.EntityContentTypeCondition',
|
||||
name: 'Example Workspace View With Entity Content Type Condition',
|
||||
element: () => import('./workspace-view.element.js'),
|
||||
meta: {
|
||||
icon: 'icon-bus',
|
||||
label: 'Conditional',
|
||||
pathname: 'conditional',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
alias: 'Umb.Condition.WorkspaceContentTypeAlias',
|
||||
//match : 'blogPost'
|
||||
oneOf: ['blogPost', 'mediaType1'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const manifests = [workspace];
|
||||
@@ -0,0 +1,19 @@
|
||||
import { html, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
@customElement('umb-example-entity-content-type-condition')
|
||||
export class UmbWorkspaceExampleViewElement extends UmbLitElement {
|
||||
override render() {
|
||||
return html`<p>
|
||||
This is a conditional element that is only shown in workspaces based on it's entities content type.
|
||||
</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbWorkspaceExampleViewElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-example-entity-content-type-condition': UmbWorkspaceExampleViewElement;
|
||||
}
|
||||
}
|
||||
7
src/Umbraco.Web.UI.Client/examples/icons/README.md
Normal file
7
src/Umbraco.Web.UI.Client/examples/icons/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Icons Example
|
||||
|
||||
This example demonstrates how to registerer your own icons.
|
||||
|
||||
Currently they have to be made as JavaScript files that exports an SVG string.
|
||||
|
||||
Declared as part of a Icon Dictionary in a JavaScript file.
|
||||
14
src/Umbraco.Web.UI.Client/examples/icons/files/icon-heart.ts
Normal file
14
src/Umbraco.Web.UI.Client/examples/icons/files/icon-heart.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export default `<!-- @license lucide-static v0.424.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-heart"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" />
|
||||
</svg>
|
||||
`;
|
||||
19
src/Umbraco.Web.UI.Client/examples/icons/files/icon-wand.ts
Normal file
19
src/Umbraco.Web.UI.Client/examples/icons/files/icon-wand.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export default `<svg
|
||||
class="lucide lucide-wand-sparkles"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72" />
|
||||
<path d="m14 7 3 3" />
|
||||
<path d="M5 6v4" />
|
||||
<path d="M19 14v4" />
|
||||
<path d="M10 2v2" />
|
||||
<path d="M7 8H3" />
|
||||
<path d="M21 16h-4" />
|
||||
<path d="M11 3H9" />
|
||||
</svg>`;
|
||||
36
src/Umbraco.Web.UI.Client/examples/icons/icons-dashboard.ts
Normal file
36
src/Umbraco.Web.UI.Client/examples/icons/icons-dashboard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { css, html, customElement, LitElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api';
|
||||
|
||||
// eslint-disable-next-line local-rules/enforce-umb-prefix-on-element-name
|
||||
@customElement('example-icons-dashboard')
|
||||
// eslint-disable-next-line local-rules/umb-class-prefix
|
||||
export class ExampleIconsDashboard extends UmbElementMixin(LitElement) {
|
||||
override render() {
|
||||
return html`
|
||||
<uui-box class="uui-text">
|
||||
<h1 class="uui-h2" style="margin-top: var(--uui-size-layout-1);">Custom icons:</h1>
|
||||
<uui-icon name="my-icon-wand"></uui-icon>
|
||||
<uui-icon name="my-icon-heart"></uui-icon>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: var(--uui-size-layout-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
export default ExampleIconsDashboard;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'example-icons-dashboard': ExampleIconsDashboard;
|
||||
}
|
||||
}
|
||||
10
src/Umbraco.Web.UI.Client/examples/icons/icons-dictionary.ts
Normal file
10
src/Umbraco.Web.UI.Client/examples/icons/icons-dictionary.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default [
|
||||
{
|
||||
name: 'my-icon-wand',
|
||||
path: () => import('./files/icon-wand.js'),
|
||||
},
|
||||
{
|
||||
name: 'my-icon-heart',
|
||||
path: () => import('./files/icon-heart.js'),
|
||||
},
|
||||
];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user