Merge remote-tracking branch 'origin/main' into feature/content-picker
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# Copy this to .env.local and change what you want to test.
|
||||
VITE_UMBRACO_INSTALL_STATUS=true
|
||||
VITE_UMBRACO_INSTALL_STATUS=running # running or must-install or must-upgrade
|
||||
VITE_UMBRACO_INSTALL_PRECONFIGURED=false
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:lit/recommended",
|
||||
"plugin:lit-a11y/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
name: Build and test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
|
||||
56
src/Umbraco.Web.UI.Client/.github/workflows/codeql.yml
vendored
Normal file
56
src/Umbraco.Web.UI.Client/.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# 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" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "main" ]
|
||||
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@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
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@v2
|
||||
8
src/Umbraco.Web.UI.Client/.storybook/main.js
Normal file
8
src/Umbraco.Web.UI.Client/.storybook/main.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-a11y'],
|
||||
framework: '@storybook/web-components',
|
||||
core: {
|
||||
builder: '@storybook/builder-vite',
|
||||
},
|
||||
};
|
||||
5
src/Umbraco.Web.UI.Client/.storybook/manager.js
Normal file
5
src/Umbraco.Web.UI.Client/.storybook/manager.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { addons } from '@storybook/addons';
|
||||
|
||||
addons.setConfig({
|
||||
enableShortcuts: false,
|
||||
});
|
||||
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>
|
||||
22
src/Umbraco.Web.UI.Client/.storybook/preview-head.html
Normal file
22
src/Umbraco.Web.UI.Client/.storybook/preview-head.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<style>
|
||||
body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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>
|
||||
29
src/Umbraco.Web.UI.Client/.storybook/preview.js
Normal file
29
src/Umbraco.Web.UI.Client/.storybook/preview.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import '@umbraco-ui/uui';
|
||||
import '@umbraco-ui/uui-css/dist/uui-css.css';
|
||||
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
|
||||
import { onUnhandledRequest } from '../src/mocks/browser';
|
||||
import { handlers } from '../src/mocks/handlers';
|
||||
|
||||
// Initialize MSW
|
||||
initialize({onUnhandledRequest});
|
||||
|
||||
// Provide the MSW addon decorator globally
|
||||
export const decorators = [mswDecorator];
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
controls: {
|
||||
expanded: true,
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: {
|
||||
global: handlers
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -14,7 +14,7 @@ As an example to show the installer instead of the login screen, set the followi
|
||||
in the `.env.local` file to indicate that Umbraco has not been installed:
|
||||
|
||||
```bash
|
||||
VITE_UMBRACO_INSTALL_STATUS=false
|
||||
VITE_UMBRACO_INSTALL_STATUS=must-install
|
||||
```
|
||||
|
||||
## Environments
|
||||
|
||||
43987
src/Umbraco.Web.UI.Client/package-lock.json
generated
43987
src/Umbraco.Web.UI.Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,69 +1,82 @@
|
||||
{
|
||||
"name": "umbraco-cms-backoffice",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"url": "https://github.com/umbraco/Umbraco.CMS.Backoffice",
|
||||
"type": "git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/umbraco/Umbraco.CMS.Backoffice/issues"
|
||||
},
|
||||
"author": {
|
||||
"name": "Umbraco A/S",
|
||||
"email": "backoffice@umbraco.com",
|
||||
"url": "https://umbraco.com"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview --open",
|
||||
"test": "web-test-runner --coverage",
|
||||
"test:watch": "web-test-runner --watch",
|
||||
"lint": "eslint . --ext .ts --cache",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier 'src/**/*.ts'",
|
||||
"format:fix": "npm run format -- --write",
|
||||
"generate:api": "npx openapi-typescript schemas/**/*.yml --output schemas/generated-schema.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17",
|
||||
"npm": ">=8.0.0 < 9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco-ui/uui": "^1.0.0-rc.1",
|
||||
"@umbraco-ui/uui-modal": "file:umbraco-ui-uui-modal-0.0.0.tgz",
|
||||
"name": "umbraco-cms-backoffice",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"url": "https://github.com/umbraco/Umbraco.CMS.Backoffice",
|
||||
"type": "git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/umbraco/Umbraco.CMS.Backoffice/issues"
|
||||
},
|
||||
"author": {
|
||||
"name": "Umbraco A/S",
|
||||
"email": "backoffice@umbraco.com",
|
||||
"url": "https://umbraco.com"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview --open",
|
||||
"test": "web-test-runner --coverage",
|
||||
"test:watch": "web-test-runner --watch",
|
||||
"lint": "eslint . --ext .ts --cache",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier 'src/**/*.ts'",
|
||||
"format:fix": "npm run format -- --write",
|
||||
"generate:api": "npx openapi-typescript schemas/**/*.yml --output schemas/generated-schema.ts",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17",
|
||||
"npm": ">=8.0.0 < 9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@umbraco-ui/uui": "^1.0.0-rc.1",
|
||||
"element-internals-polyfill": "^1.1.6",
|
||||
"lit": "^2.2.8",
|
||||
"openapi-typescript-fetch": "^1.1.3",
|
||||
"router-slot": "^1.5.5",
|
||||
"rxjs": "^7.5.6",
|
||||
"@umbraco-ui/uui-modal": "file:umbraco-ui-uui-modal-0.0.0.tgz",
|
||||
"@umbraco-ui/uui-modal-container": "file:umbraco-ui-uui-modal-container-0.0.0.tgz",
|
||||
"@umbraco-ui/uui-modal-dialog": "file:umbraco-ui-uui-modal-dialog-0.0.0.tgz",
|
||||
"@umbraco-ui/uui-modal-sidebar": "file:umbraco-ui-uui-modal-sidebar-0.0.0.tgz",
|
||||
"element-internals-polyfill": "^1.1.4",
|
||||
"lit": "^2.2.7",
|
||||
"openapi-typescript-fetch": "^1.1.3",
|
||||
"router-slot": "^1.5.5",
|
||||
"rxjs": "^7.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@open-wc/testing": "^3.1.6",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"@typescript-eslint/parser": "^5.30.0",
|
||||
"@web/dev-server-esbuild": "^0.3.1",
|
||||
"@web/test-runner": "^0.13.31",
|
||||
"@web/test-runner-playwright": "^0.8.9",
|
||||
"eslint": "^8.18.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.1.3",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"eslint-plugin-lit-a11y": "^2.2.0",
|
||||
"msw": "^0.42.3",
|
||||
"prettier": "2.7.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^2.9.13"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
"@umbraco-ui/uui-modal-sidebar": "file:umbraco-ui-uui-modal-sidebar-0.0.0.tgz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.10",
|
||||
"@open-wc/testing": "^3.1.6",
|
||||
"@storybook/addon-a11y": "^6.5.9",
|
||||
"@storybook/addon-actions": "^6.5.9",
|
||||
"@storybook/addon-essentials": "^6.5.9",
|
||||
"@storybook/addon-links": "^6.5.9",
|
||||
"@storybook/builder-vite": "^0.2.2",
|
||||
"@storybook/web-components": "^6.5.9",
|
||||
"@types/chai": "^4.3.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.32.0",
|
||||
"@typescript-eslint/parser": "^5.32.0",
|
||||
"@web/dev-server-esbuild": "^0.3.1",
|
||||
"@web/test-runner": "^0.13.31",
|
||||
"@web/test-runner-playwright": "^0.8.9",
|
||||
"babel-loader": "^8.2.5",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.4.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"eslint-plugin-lit-a11y": "^2.2.2",
|
||||
"eslint-plugin-storybook": "^0.6.3",
|
||||
"lit-html": "^2.2.7",
|
||||
"msw": "^0.44.2",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"prettier": "2.7.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.3"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker (0.42.3).
|
||||
* Mock Service Worker (0.44.2).
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929'
|
||||
const bypassHeaderName = 'x-msw-bypass'
|
||||
const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995'
|
||||
const activeClientIds = new Set()
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
return self.skipWaiting()
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', async function (event) {
|
||||
return self.clients.claim()
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('message', async function (event) {
|
||||
@@ -33,7 +32,9 @@ self.addEventListener('message', async function (event) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll()
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
@@ -83,161 +84,6 @@ self.addEventListener('message', async function (event) {
|
||||
}
|
||||
})
|
||||
|
||||
// Resolve the "main" client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (client.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll()
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const clonedResponse = response.clone()
|
||||
sendToClient(client, {
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
type: clonedResponse.type,
|
||||
ok: clonedResponse.ok,
|
||||
status: clonedResponse.status,
|
||||
statusText: clonedResponse.statusText,
|
||||
body:
|
||||
clonedResponse.body === null ? null : await clonedResponse.text(),
|
||||
headers: serializeHeaders(clonedResponse.headers),
|
||||
redirected: clonedResponse.redirected,
|
||||
},
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
const requestClone = request.clone()
|
||||
const getOriginalResponse = () => fetch(requestClone)
|
||||
|
||||
// Bypass mocking when the request client is not active.
|
||||
if (!client) {
|
||||
return getOriginalResponse()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return await getOriginalResponse()
|
||||
}
|
||||
|
||||
// Bypass requests with the explicit bypass header
|
||||
if (requestClone.headers.get(bypassHeaderName) === 'true') {
|
||||
const cleanRequestHeaders = serializeHeaders(requestClone.headers)
|
||||
|
||||
// Remove the bypass header to comply with the CORS preflight check.
|
||||
delete cleanRequestHeaders[bypassHeaderName]
|
||||
|
||||
const originalRequest = new Request(requestClone, {
|
||||
headers: new Headers(cleanRequestHeaders),
|
||||
})
|
||||
|
||||
return fetch(originalRequest)
|
||||
}
|
||||
|
||||
// Send the request to the client-side MSW.
|
||||
const reqHeaders = serializeHeaders(request.headers)
|
||||
const body = await request.text()
|
||||
|
||||
const clientMessage = await sendToClient(client, {
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: reqHeaders,
|
||||
cache: request.cache,
|
||||
mode: request.mode,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body,
|
||||
bodyUsed: request.bodyUsed,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
})
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_SUCCESS': {
|
||||
return delayPromise(
|
||||
() => respondWithMock(clientMessage),
|
||||
clientMessage.payload.delay,
|
||||
)
|
||||
}
|
||||
|
||||
case 'MOCK_NOT_FOUND': {
|
||||
return getOriginalResponse()
|
||||
}
|
||||
|
||||
case 'NETWORK_ERROR': {
|
||||
const { name, message } = clientMessage.payload
|
||||
const networkError = new Error(message)
|
||||
networkError.name = name
|
||||
|
||||
// Rejecting a request Promise emulates a network error.
|
||||
throw networkError
|
||||
}
|
||||
|
||||
case 'INTERNAL_ERROR': {
|
||||
const parsedBody = JSON.parse(clientMessage.payload.body)
|
||||
|
||||
console.error(
|
||||
`\
|
||||
[MSW] Uncaught exception in the request handler for "%s %s":
|
||||
|
||||
${parsedBody.location}
|
||||
|
||||
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
|
||||
`,
|
||||
request.method,
|
||||
request.url,
|
||||
)
|
||||
|
||||
return respondWithMock(clientMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return getOriginalResponse()
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
const accept = request.headers.get('accept') || ''
|
||||
@@ -265,9 +111,10 @@ self.addEventListener('fetch', function (event) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = uuidv4()
|
||||
// Generate unique request ID.
|
||||
const requestId = Math.random().toString(16).slice(2)
|
||||
|
||||
return event.respondWith(
|
||||
event.respondWith(
|
||||
handleRequest(event, requestId).catch((error) => {
|
||||
if (error.name === 'NetworkError') {
|
||||
console.warn(
|
||||
@@ -290,14 +137,142 @@ self.addEventListener('fetch', function (event) {
|
||||
)
|
||||
})
|
||||
|
||||
function serializeHeaders(headers) {
|
||||
const reqHeaders = {}
|
||||
headers.forEach((value, name) => {
|
||||
reqHeaders[name] = reqHeaders[name]
|
||||
? [].concat(reqHeaders[name]).concat(value)
|
||||
: value
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const clonedResponse = response.clone()
|
||||
sendToClient(client, {
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
type: clonedResponse.type,
|
||||
ok: clonedResponse.ok,
|
||||
status: clonedResponse.status,
|
||||
statusText: clonedResponse.statusText,
|
||||
body:
|
||||
clonedResponse.body === null ? null : await clonedResponse.text(),
|
||||
headers: Object.fromEntries(clonedResponse.headers.entries()),
|
||||
redirected: clonedResponse.redirected,
|
||||
},
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (client.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
return reqHeaders
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
const clonedRequest = request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the cilent).
|
||||
const headers = Object.fromEntries(clonedRequest.headers.entries())
|
||||
|
||||
// Remove MSW-specific request headers so the bypassed requests
|
||||
// comply with the server's CORS preflight check.
|
||||
// Operate with the headers as an object because request "Headers"
|
||||
// are immutable.
|
||||
delete headers['x-msw-bypass']
|
||||
|
||||
return fetch(clonedRequest, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass requests with the explicit bypass header.
|
||||
// Such requests can be issued by "ctx.fetch()".
|
||||
if (request.headers.get('x-msw-bypass') === 'true') {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const clientMessage = await sendToClient(client, {
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
mode: request.mode,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.text(),
|
||||
bodyUsed: request.bodyUsed,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
})
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'MOCK_NOT_FOUND': {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
case 'NETWORK_ERROR': {
|
||||
const { name, message } = clientMessage.data
|
||||
const networkError = new Error(message)
|
||||
networkError.name = name
|
||||
|
||||
// Rejecting a "respondWith" promise emulates a network error.
|
||||
throw networkError
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
function sendToClient(client, message) {
|
||||
@@ -312,27 +287,17 @@ function sendToClient(client, message) {
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(JSON.stringify(message), [channel.port2])
|
||||
client.postMessage(message, [channel.port2])
|
||||
})
|
||||
}
|
||||
|
||||
function delayPromise(cb, duration) {
|
||||
function sleep(timeMs) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(cb()), duration)
|
||||
setTimeout(resolve, timeMs)
|
||||
})
|
||||
}
|
||||
|
||||
function respondWithMock(clientMessage) {
|
||||
return new Response(clientMessage.payload.body, {
|
||||
...clientMessage.payload,
|
||||
headers: clientMessage.payload.headers,
|
||||
})
|
||||
}
|
||||
|
||||
function uuidv4() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
async function respondWithMock(response) {
|
||||
await sleep(response.delay)
|
||||
return new Response(response.body, response)
|
||||
}
|
||||
|
||||
@@ -89,6 +89,35 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
/upgrade/settings:
|
||||
get:
|
||||
operationId: GetUpgradeSettings
|
||||
responses:
|
||||
'200':
|
||||
description: 200 response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpgradeSettingsResponse'
|
||||
default:
|
||||
description: default response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
/upgrade/authorize:
|
||||
post:
|
||||
operationId: PostUpgradeAuthorize
|
||||
parameters: []
|
||||
responses:
|
||||
'201':
|
||||
description: 201 response
|
||||
'400':
|
||||
description: 400 response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProblemDetails'
|
||||
/user:
|
||||
get:
|
||||
operationId: GetUser
|
||||
@@ -339,6 +368,25 @@ components:
|
||||
type: string
|
||||
required:
|
||||
- version
|
||||
UpgradeSettingsResponse:
|
||||
type: object
|
||||
properties:
|
||||
currentState:
|
||||
type: string
|
||||
newState:
|
||||
type: string
|
||||
newVersion:
|
||||
type: string
|
||||
oldVersion:
|
||||
type: string
|
||||
reportUrl:
|
||||
type: string
|
||||
required:
|
||||
- currentState
|
||||
- newState
|
||||
- newVersion
|
||||
- oldVersion
|
||||
- reportUrl
|
||||
UserResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -19,6 +19,12 @@ export interface paths {
|
||||
"/server/version": {
|
||||
get: operations["GetVersion"];
|
||||
};
|
||||
"/upgrade/settings": {
|
||||
get: operations["GetUpgradeSettings"];
|
||||
};
|
||||
"/upgrade/authorize": {
|
||||
post: operations["PostUpgradeAuthorize"];
|
||||
};
|
||||
"/user": {
|
||||
get: operations["GetUser"];
|
||||
};
|
||||
@@ -107,6 +113,13 @@ export interface components {
|
||||
VersionResponse: {
|
||||
version: string;
|
||||
};
|
||||
UpgradeSettingsResponse: {
|
||||
currentState: string;
|
||||
newState: string;
|
||||
newVersion: string;
|
||||
oldVersion: string;
|
||||
reportUrl: string;
|
||||
};
|
||||
UserResponse: {
|
||||
username: string;
|
||||
role: string;
|
||||
@@ -207,6 +220,35 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
GetUpgradeSettings: {
|
||||
responses: {
|
||||
/** 200 response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpgradeSettingsResponse"];
|
||||
};
|
||||
};
|
||||
/** default response */
|
||||
default: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
PostUpgradeAuthorize: {
|
||||
parameters: {};
|
||||
responses: {
|
||||
/** 201 response */
|
||||
201: unknown;
|
||||
/** 400 response */
|
||||
400: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ProblemDetails"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
GetUser: {
|
||||
responses: {
|
||||
/** 200 response */
|
||||
|
||||
@@ -4,13 +4,13 @@ import 'router-slot';
|
||||
import { UUIIconRegistryEssential } from '@umbraco-ui/uui';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { Guard, IRoute } from 'router-slot/model';
|
||||
|
||||
import { getServerStatus } from './core/api/fetcher';
|
||||
import { UmbContextProviderMixin } from './core/context';
|
||||
import { UmbExtensionManifest, UmbExtensionManifestCore, UmbExtensionRegistry } from './core/extension';
|
||||
import { ServerStatus } from './core/models';
|
||||
import { internalManifests } from './temp-internal-manifests';
|
||||
import { IRoute } from 'router-slot/model';
|
||||
|
||||
@customElement('umb-app')
|
||||
export class UmbApp extends UmbContextProviderMixin(LitElement) {
|
||||
@@ -36,12 +36,12 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
|
||||
{
|
||||
path: 'upgrade',
|
||||
component: () => import('./upgrader/upgrader.element'),
|
||||
guards: [this._isAuthorizedGuard.bind(this)],
|
||||
guards: [this._isAuthorizedGuard('/upgrade')],
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
component: () => import('./backoffice/backoffice.element'),
|
||||
guards: [this._isAuthorizedGuard.bind(this)],
|
||||
guards: [this._isAuthorizedGuard()],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -98,13 +98,21 @@ export class UmbApp extends UmbContextProviderMixin(LitElement) {
|
||||
return sessionStorage.getItem('is-authenticated') === 'true';
|
||||
}
|
||||
|
||||
private _isAuthorizedGuard(): boolean {
|
||||
if (this._isAuthorized()) {
|
||||
return true;
|
||||
}
|
||||
private _isAuthorizedGuard(redirectTo?: string): Guard {
|
||||
return () => {
|
||||
if (this._isAuthorized()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
history.replaceState(null, '', '/login');
|
||||
return false;
|
||||
let returnPath = '/login';
|
||||
|
||||
if (redirectTo) {
|
||||
returnPath += `?redirectTo=${redirectTo}`;
|
||||
}
|
||||
|
||||
history.replaceState(null, '', returnPath);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
private async _registerExtensionManifestsFromServer() {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import '../auth-layout.element';
|
||||
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, CSSResultGroup, html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { query } from 'router-slot';
|
||||
|
||||
import { postUserLogin } from '../../core/api/fetcher';
|
||||
|
||||
import '../auth-layout.element';
|
||||
|
||||
@customElement('umb-login')
|
||||
export default class UmbLogin extends LitElement {
|
||||
static styles: CSSResultGroup = [
|
||||
@@ -46,7 +47,11 @@ export default class UmbLogin extends LitElement {
|
||||
try {
|
||||
await postUserLogin({ username, password, persist });
|
||||
this._loggingIn = false;
|
||||
history.pushState(null, '', '/section');
|
||||
let { redirectTo } = query();
|
||||
if (!redirectTo) {
|
||||
redirectTo = '/section';
|
||||
}
|
||||
history.pushState(null, '', redirectTo);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this._loggingIn = false;
|
||||
|
||||
@@ -17,3 +17,5 @@ export const getUserSections = fetcher.path('/user/sections').method('get').crea
|
||||
export const getInstallSettings = fetcher.path('/install/settings').method('get').create();
|
||||
export const postInstallValidateDatabase = fetcher.path('/install/validateDatabase').method('post').create();
|
||||
export const postInstallSetup = fetcher.path('/install/setup').method('post').create();
|
||||
export const getUpgradeSettings = fetcher.path('/upgrade/settings').method('get').create();
|
||||
export const PostUpgradeAuthorize = fetcher.path('/upgrade/authorize').method('post').create();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import { UmbContextProviderMixin } from './context-provider.mixin';
|
||||
|
||||
@customElement('umb-context-provider')
|
||||
export class UmbContextProviderElement extends UmbContextProviderMixin(LitElement) {
|
||||
/**
|
||||
* The value to provide to the context.
|
||||
* @required
|
||||
*/
|
||||
@property({ type: Object })
|
||||
value!: unknown;
|
||||
|
||||
/**
|
||||
* The key to provide to the context.
|
||||
* @required
|
||||
*/
|
||||
@property({ type: String })
|
||||
key!: string;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.key) {
|
||||
throw new Error('The key property is required.');
|
||||
}
|
||||
if (!this.value) {
|
||||
throw new Error('The value property is required.');
|
||||
}
|
||||
this.provideContext(this.key, this.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export type ProblemDetails = components['schemas']['ProblemDetails'];
|
||||
export type UserResponse = components['schemas']['UserResponse'];
|
||||
export type AllowedSectionsResponse = components['schemas']['AllowedSectionsResponse'];
|
||||
export type UmbracoInstaller = components['schemas']['InstallSettingsResponse'];
|
||||
export type UmbracoUpgrader = components['schemas']['UpgradeSettingsResponse'];
|
||||
|
||||
// Models
|
||||
export type UmbracoPerformInstallDatabaseConfiguration = components['schemas']['InstallSetupDatabaseConfiguration'];
|
||||
|
||||
6
src/Umbraco.Web.UI.Client/src/installer/index.ts
Normal file
6
src/Umbraco.Web.UI.Client/src/installer/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './installer-consent.element';
|
||||
export * from './installer-database.element';
|
||||
export * from './installer-installing.element';
|
||||
export * from './installer-user.element';
|
||||
export * from './installer-layout.element';
|
||||
export * from './installer.element';
|
||||
@@ -2,6 +2,7 @@ import { css, CSSResultGroup, html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../core/context';
|
||||
import { TelemetryModel } from '../core/models';
|
||||
import { UmbInstallerContext } from './installer-context';
|
||||
@@ -48,10 +49,10 @@ export class UmbInstallerConsent extends UmbContextConsumerMixin(LitElement) {
|
||||
private _telemetryLevels: TelemetryModel[] = [];
|
||||
|
||||
@state()
|
||||
private _telemetryFormData!: TelemetryModel['level'];
|
||||
private _telemetryFormData?: TelemetryModel['level'];
|
||||
|
||||
@state()
|
||||
private _installerStore!: UmbInstallerContext;
|
||||
private _installerStore?: UmbInstallerContext;
|
||||
|
||||
private storeDataSubscription?: Subscription;
|
||||
private storeSettingsSubscription?: Subscription;
|
||||
@@ -85,7 +86,7 @@ export class UmbInstallerConsent extends UmbContextConsumerMixin(LitElement) {
|
||||
|
||||
const value: { [key: string]: string } = {};
|
||||
value[target.name] = this._telemetryLevels[parseInt(target.value) - 1].level;
|
||||
this._installerStore.appendData(value);
|
||||
this._installerStore?.appendData(value);
|
||||
}
|
||||
|
||||
private _onNext() {
|
||||
@@ -105,7 +106,7 @@ export class UmbInstallerConsent extends UmbContextConsumerMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderSlider() {
|
||||
if (!this._telemetryLevels) return;
|
||||
if (!this._telemetryLevels || this._telemetryLevels.length < 1) return;
|
||||
|
||||
return html`
|
||||
<uui-slider
|
||||
@@ -125,7 +126,7 @@ export class UmbInstallerConsent extends UmbContextConsumerMixin(LitElement) {
|
||||
render() {
|
||||
return html`
|
||||
<div id="container" class="uui-text">
|
||||
<h1>Consent Level</h1>
|
||||
<h1>Consent for telemetry data</h1>
|
||||
${this._renderSlider()}
|
||||
<div id="buttons">
|
||||
<uui-button label="Back" @click=${this._onBack} look="secondary"></uui-button>
|
||||
|
||||
@@ -4,11 +4,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../core/context';
|
||||
import {
|
||||
ProblemDetails,
|
||||
UmbracoInstallerDatabaseModel,
|
||||
UmbracoPerformInstallDatabaseConfiguration,
|
||||
} from '../core/models';
|
||||
import { ProblemDetails, UmbracoInstallerDatabaseModel, UmbracoPerformInstallDatabaseConfiguration } from '../core/models';
|
||||
import { UmbInstallerContext } from './installer-context';
|
||||
|
||||
@customElement('umb-installer-database')
|
||||
@@ -99,7 +95,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
private _preConfiguredDatabase?: UmbracoInstallerDatabaseModel;
|
||||
|
||||
@state()
|
||||
private _installerStore!: UmbInstallerContext;
|
||||
private _installerStore?: UmbInstallerContext;
|
||||
|
||||
private storeDataSubscription?: Subscription;
|
||||
private storeSettingsSubscription?: Subscription;
|
||||
@@ -142,9 +138,9 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
const value: { [key: string]: string | boolean } = {};
|
||||
value[target.name] = target.checked ?? target.value; // handle boolean and text inputs
|
||||
|
||||
const database = { ...this._installerStore.getData().database, ...value };
|
||||
const database = { ...this._installerStore?.getData().database, ...value };
|
||||
|
||||
this._installerStore.appendData({ database });
|
||||
this._installerStore?.appendData({ database });
|
||||
}
|
||||
|
||||
private _handleSubmit = (e: SubmitEvent) => {
|
||||
@@ -167,7 +163,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
const useIntegratedAuthentication = formData.has('useIntegratedAuthentication');
|
||||
|
||||
const database = {
|
||||
...this._installerStore.getData().database,
|
||||
...this._installerStore?.getData().database,
|
||||
id,
|
||||
username,
|
||||
password,
|
||||
@@ -176,14 +172,15 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
useIntegratedAuthentication,
|
||||
} as UmbracoPerformInstallDatabaseConfiguration;
|
||||
|
||||
this._installerStore.appendData({ database });
|
||||
this._installerStore?.appendData({ database });
|
||||
}
|
||||
|
||||
this._installerStore.requestInstall().then(this._handleFulfilled.bind(this), this._handleRejected.bind(this));
|
||||
this._installerStore?.requestInstall().then(this._handleFulfilled.bind(this), this._handleRejected.bind(this));
|
||||
this._installButton.state = 'waiting';
|
||||
};
|
||||
private _handleFulfilled() {
|
||||
this.dispatchEvent(new CustomEvent('next', { bubbles: true, composed: true }));
|
||||
this._installButton.state = undefined;
|
||||
}
|
||||
private _handleRejected(error: ProblemDetails) {
|
||||
this._installButton.state = 'failed';
|
||||
@@ -195,7 +192,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
}
|
||||
|
||||
private get selectedDatabase() {
|
||||
const id = this._installerStore.getData().database?.id;
|
||||
const id = this._installerStore?.getData().database?.id;
|
||||
console.log('selected id', id, this._databases);
|
||||
return this._databases.find((x) => x.id === id) ?? this._databases[0];
|
||||
}
|
||||
@@ -213,7 +210,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
result.push(this._renderServer());
|
||||
}
|
||||
|
||||
result.push(this._renderDatabaseName());
|
||||
result.push(this._renderDatabaseName(this.databaseFormData.name ?? this.selectedDatabase.defaultDatabaseName));
|
||||
|
||||
if (this.selectedDatabase.requiresCredentials) {
|
||||
result.push(this._renderCredentials());
|
||||
@@ -239,15 +236,15 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
|
||||
</uui-form-layout-item>
|
||||
`;
|
||||
|
||||
private _renderDatabaseName = () => html` <uui-form-layout-item>
|
||||
private _renderDatabaseName = (value: string) => html` <uui-form-layout-item>
|
||||
<uui-label for="database-name" slot="label" required>Database Name</uui-label>
|
||||
<uui-input
|
||||
type="text"
|
||||
.value=${this.databaseFormData.name ?? ''}
|
||||
.value=${value}
|
||||
id="database-name"
|
||||
name="name"
|
||||
@input=${this._handleChange}
|
||||
placeholder="umbraco-cms"
|
||||
placeholder="umbraco"
|
||||
required
|
||||
required-message="Database name is required"></uui-input>
|
||||
</uui-form-layout-item>`;
|
||||
|
||||
@@ -50,17 +50,17 @@ export class UmbInstallerLayout extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`<div>
|
||||
<div id="background"></div>
|
||||
<div id="background" aria-hidden="true"></div>
|
||||
|
||||
<div id="logo">
|
||||
<div id="logo" aria-hidden="true">
|
||||
<img src="/umbraco_logo_white.svg" alt="Umbraco" />
|
||||
</div>
|
||||
|
||||
<div id="container">
|
||||
<main id="container">
|
||||
<div id="box">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
|
||||
];
|
||||
|
||||
@state()
|
||||
private _userFormData!: { name: string; password: string; email: string; subscribeToNewsletter: boolean };
|
||||
private _userFormData?: { name: string; password: string; email: string; subscribeToNewsletter: boolean };
|
||||
|
||||
@state()
|
||||
private _installerStore!: UmbInstallerContext;
|
||||
private _installerStore?: UmbInstallerContext;
|
||||
|
||||
private installerStoreSubscription?: Subscription;
|
||||
|
||||
@@ -101,7 +101,7 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
|
||||
const email = formData.get('email');
|
||||
const subscribeToNewsletter = formData.has('subscribeToNewsletter');
|
||||
|
||||
this._installerStore.appendData({ user: { name, password, email, subscribeToNewsletter } });
|
||||
this._installerStore?.appendData({ user: { name, password, email, subscribeToNewsletter } });
|
||||
this.dispatchEvent(new CustomEvent('next', { bubbles: true, composed: true }));
|
||||
};
|
||||
|
||||
@@ -115,7 +115,7 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
|
||||
<uui-input
|
||||
type="text"
|
||||
id="name"
|
||||
.value=${this._userFormData.name}
|
||||
.value=${this._userFormData?.name}
|
||||
name="name"
|
||||
required
|
||||
required-message="Name is required"></uui-input>
|
||||
@@ -126,7 +126,7 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
|
||||
<uui-input
|
||||
type="email"
|
||||
id="email"
|
||||
.value=${this._userFormData.email}
|
||||
.value=${this._userFormData?.email}
|
||||
name="email"
|
||||
required
|
||||
required-message="Email is required"></uui-input>
|
||||
@@ -137,7 +137,7 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
|
||||
<uui-input-password
|
||||
id="password"
|
||||
name="password"
|
||||
.value=${this._userFormData.password}
|
||||
.value=${this._userFormData?.password}
|
||||
required
|
||||
required-message="Password is required"></uui-input-password>
|
||||
</uui-form-layout-item>
|
||||
@@ -146,7 +146,7 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
|
||||
<uui-checkbox
|
||||
name="subscribeToNewsletter"
|
||||
label="Remember me"
|
||||
.checked=${this._userFormData.subscribeToNewsletter}>
|
||||
.checked=${this._userFormData?.subscribeToNewsletter || false}>
|
||||
Keep me updated on Umbraco Versions, Security Bulletins and Community News
|
||||
</uui-checkbox>
|
||||
</uui-form-layout-item>
|
||||
|
||||
93
src/Umbraco.Web.UI.Client/src/installer/installer.stories.ts
Normal file
93
src/Umbraco.Web.UI.Client/src/installer/installer.stories.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import '../core/context/context-provider.element';
|
||||
import './installer-consent.element';
|
||||
import './installer-database.element';
|
||||
import './installer-installing.element';
|
||||
import './installer-user.element';
|
||||
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { UmbInstallerUser } from '.';
|
||||
import { UmbracoInstaller } from '../core/models';
|
||||
import { UmbInstallerContext } from './installer-context';
|
||||
|
||||
export default {
|
||||
title: 'Components/Installer/Steps',
|
||||
component: 'umb-installer',
|
||||
id: 'installer',
|
||||
decorators: [
|
||||
(story) =>
|
||||
html`<umb-context-provider
|
||||
style="display: block;margin: 2rem 25%;padding: 1rem;border: 1px solid #ddd;"
|
||||
key="umbInstallerContext"
|
||||
.value=${new UmbInstallerContext()}>
|
||||
${story()}
|
||||
</umb-context-provider>`,
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
export const Step1User: Story<UmbInstallerUser> = () => html`<umb-installer-user></umb-installer-user>`;
|
||||
Step1User.storyName = 'Step 1: User';
|
||||
Step1User.parameters = {
|
||||
actions: {
|
||||
handles: ['next'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Step2Telemetry: Story = () => html`<umb-installer-consent></umb-installer-consent>`;
|
||||
Step2Telemetry.storyName = 'Step 2: Telemetry data';
|
||||
Step2Telemetry.parameters = {
|
||||
actions: {
|
||||
handles: ['previous', 'next'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Step3Database: Story = () => html`<umb-installer-database></umb-installer-database>`;
|
||||
Step3Database.storyName = 'Step 3: Database';
|
||||
Step3Database.parameters = {
|
||||
actions: {
|
||||
handles: ['previous', 'next'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Step3DatabasePreconfigured: Story = () => html`<umb-installer-database></umb-installer-database>`;
|
||||
Step3DatabasePreconfigured.storyName = 'Step 3: Database (preconfigured)';
|
||||
Step3DatabasePreconfigured.parameters = {
|
||||
actions: {
|
||||
handles: ['previous', 'next'],
|
||||
},
|
||||
msw: {
|
||||
handlers: {
|
||||
global: null,
|
||||
others: [
|
||||
rest.get('/umbraco/backoffice/install/settings', (_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json<UmbracoInstaller>({
|
||||
user: { consentLevels: [], minCharLength: 2, minNonAlphaNumericLength: 2 },
|
||||
databases: [
|
||||
{
|
||||
id: '1',
|
||||
sortOrder: -1,
|
||||
displayName: 'SQLite',
|
||||
defaultDatabaseName: 'Umbraco',
|
||||
providerName: 'Microsoft.Data.SQLite',
|
||||
isConfigured: true,
|
||||
requiresServer: false,
|
||||
serverPlaceholder: null,
|
||||
requiresCredentials: false,
|
||||
supportsIntegratedAuthentication: false,
|
||||
requiresConnectionTest: false,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Step4Installing: Story = () => html`<umb-installer-installing></umb-installer-installing>`;
|
||||
Step4Installing.storyName = 'Step 4: Installing';
|
||||
103
src/Umbraco.Web.UI.Client/src/installer/installer.test.ts
Normal file
103
src/Umbraco.Web.UI.Client/src/installer/installer.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { html, fixture, expect } from '@open-wc/testing';
|
||||
import { UmbInstallerConsent } from './installer-consent.element';
|
||||
import { UmbInstallerDatabase } from './installer-database.element';
|
||||
import { UmbInstallerInstalling } from './installer-installing.element';
|
||||
import { UmbInstallerLayout } from './installer-layout.element';
|
||||
import { UmbInstallerUser } from './installer-user.element';
|
||||
import { UmbInstaller } from './installer.element';
|
||||
|
||||
describe('UmbInstaller', () => {
|
||||
let element: UmbInstaller;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-installer></umb-installer>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', async () => {
|
||||
expect(element).to.be.instanceOf(UmbInstaller);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
expect(element).shadowDom.to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UmbInstallerLayout', () => {
|
||||
let element: UmbInstallerLayout;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-installer-layout></umb-installer-layout>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', async () => {
|
||||
expect(element).to.be.instanceOf(UmbInstallerLayout);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
expect(element).shadowDom.to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UmbInstallerUser', () => {
|
||||
let element: UmbInstallerUser;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-installer-user></umb-installer-user>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', async () => {
|
||||
expect(element).to.be.instanceOf(UmbInstallerUser);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
expect(element).shadowDom.to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UmbInstallerConsent', () => {
|
||||
let element: UmbInstallerConsent;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-installer-consent></umb-installer-consent>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', async () => {
|
||||
expect(element).to.be.instanceOf(UmbInstallerConsent);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
expect(element).shadowDom.to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UmbInstallerDatabase', () => {
|
||||
let element: UmbInstallerDatabase;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-installer-database></umb-installer-database>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', async () => {
|
||||
expect(element).to.be.instanceOf(UmbInstallerDatabase);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
expect(element).shadowDom.to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UmbInstallerInstalling', () => {
|
||||
let element: UmbInstallerInstalling;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-installer-installing></umb-installer-installing>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', async () => {
|
||||
expect(element).to.be.instanceOf(UmbInstallerInstalling);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
expect(element).shadowDom.to.be.accessible();
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,18 @@
|
||||
import { setupWorker } from 'msw';
|
||||
import { MockedRequest, setupWorker } from 'msw';
|
||||
|
||||
import { handlers } from './handlers';
|
||||
|
||||
const worker = setupWorker(...handlers);
|
||||
|
||||
export const onUnhandledRequest = (req: MockedRequest) => {
|
||||
if (req.url.pathname.startsWith('/node_modules/')) return;
|
||||
if (req.url.pathname.startsWith('/src/')) return;
|
||||
if (req.destination === 'image') return;
|
||||
|
||||
console.warn('Found an unhandled %s request to %s', req.method, req.url.href);
|
||||
};
|
||||
|
||||
export const startMockServiceWorker = () =>
|
||||
worker.start({
|
||||
onUnhandledRequest: (req) => {
|
||||
if (req.url.pathname.startsWith('/node_modules/')) return;
|
||||
if (req.url.pathname.startsWith('/src/')) return;
|
||||
if (req.destination === 'image') return;
|
||||
|
||||
console.warn('Found an unhandled %s request to %s', req.method, req.url.href);
|
||||
},
|
||||
onUnhandledRequest,
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export const handlers = [
|
||||
{
|
||||
level: 'Detailed',
|
||||
description:
|
||||
'We will send:\n <br>- Anonymized site ID, umbraco version, and packages installed.\n <br>- Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use.\n <br>- System information: Webserver, server OS, server framework, server OS language, and database provider.\n <br>- Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.\n <br>\n <br><i>We might change what we send on the Detailed level in the future. If so, it will be listed above.\n <br>By choosing "Detailed" you agree to current and future anonymized information being collected.</i>',
|
||||
'We will send:<ul><li>Anonymized site ID, umbraco version, and packages installed.</li><li>Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use.</li><li>System information: Webserver, server OS, server framework, server OS language, and database provider.</li><li>Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.</li></ul><i>We might change what we send on the Detailed level in the future. If so, it will be listed above.<br>By choosing "Detailed" you agree to current and future anonymized information being collected.</i>',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ export const handlers = [
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<StatusResponse>({
|
||||
serverStatus: import.meta.env.VITE_UMBRACO_INSTALL_STATUS !== 'false' ? 'running' : 'must-install',
|
||||
serverStatus: import.meta.env.VITE_UMBRACO_INSTALL_STATUS,
|
||||
})
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { PostInstallRequest, UmbracoUpgrader } from '../../core/models';
|
||||
|
||||
export const handlers = [
|
||||
rest.get('/umbraco/backoffice/upgrade/settings', (_req, res, ctx) => {
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(200),
|
||||
ctx.json<UmbracoUpgrader>({
|
||||
currentState: '2b20c6e7',
|
||||
newState: '2b20c6e8',
|
||||
oldVersion: '13.0.0',
|
||||
newVersion: '13.1.0',
|
||||
reportUrl: 'https://our.umbraco.com/download/releases/1000',
|
||||
})
|
||||
);
|
||||
}),
|
||||
|
||||
rest.post<PostInstallRequest>('/umbraco/backoffice/upgrade/authorize', async (_req, res, ctx) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000)); // simulate a delay of 1-2 seconds
|
||||
|
||||
return res(
|
||||
// Respond with a 200 status code
|
||||
ctx.status(201)
|
||||
);
|
||||
}),
|
||||
];
|
||||
@@ -2,12 +2,14 @@ import { handlers as contentHandlers } from './domains/content.handlers';
|
||||
import { handlers as installHandlers } from './domains/install.handlers';
|
||||
import { handlers as manifestsHandlers } from './domains/manifests.handlers';
|
||||
import { handlers as serverHandlers } from './domains/server.handlers';
|
||||
import { handlers as upgradeHandlers } from './domains/upgrade.handlers';
|
||||
import { handlers as userHandlers } from './domains/user.handlers';
|
||||
|
||||
export const handlers = [
|
||||
...serverHandlers,
|
||||
...contentHandlers,
|
||||
...installHandlers,
|
||||
...upgradeHandlers,
|
||||
...manifestsHandlers,
|
||||
...userHandlers,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import '../core/context/context-provider.element';
|
||||
import '../installer/installer.element';
|
||||
|
||||
import { Meta } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
export default {
|
||||
title: 'Pages/Installer',
|
||||
component: 'umb-installer',
|
||||
id: 'installer-page',
|
||||
} as Meta;
|
||||
|
||||
export const Installer = () => html`<umb-installer></umb-installer>`;
|
||||
2
src/Umbraco.Web.UI.Client/src/upgrader/index.ts
Normal file
2
src/Umbraco.Web.UI.Client/src/upgrader/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './upgrader.element';
|
||||
export * from './upgrader-view.element';
|
||||
106
src/Umbraco.Web.UI.Client/src/upgrader/upgrader-view.element.ts
Normal file
106
src/Umbraco.Web.UI.Client/src/upgrader/upgrader-view.element.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
|
||||
import { UmbracoUpgrader } from '../core/models';
|
||||
|
||||
/**
|
||||
* @element umb-upgrader-view
|
||||
* @fires {CustomEvent<SubmitEvent>} onAuthorizeUpgrade - fires when the user clicks the continue button
|
||||
*/
|
||||
@customElement('umb-upgrader-view')
|
||||
export class UmbUpgraderView extends LitElement {
|
||||
static styles: CSSResultGroup = [
|
||||
css`
|
||||
.center {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
.error {
|
||||
color: var(--uui-color-danger);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@property({ type: Boolean })
|
||||
fetching = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
upgrading = false;
|
||||
|
||||
@property({ type: String })
|
||||
errorMessage = '';
|
||||
|
||||
@property({ type: Object, reflect: true })
|
||||
settings?: UmbracoUpgrader;
|
||||
|
||||
private _renderLayout() {
|
||||
return html`
|
||||
<h1>Upgrading Umbraco</h1>
|
||||
|
||||
<p>
|
||||
Welcome to the Umbraco installer. You see this screen because your Umbraco installation needs a quick upgrade
|
||||
of its database and files, which will ensure your website is kept as fast, secure and up to date as possible.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Detected current version <strong>${this.settings?.oldVersion}</strong> (${this.settings?.currentState}),
|
||||
which needs to be upgraded to <strong>${this.settings?.newVersion}</strong> (${this.settings?.newState}).
|
||||
To compare versions and read a report of changes between versions, use the View Report button below.
|
||||
</p>
|
||||
|
||||
${
|
||||
this.settings?.reportUrl
|
||||
? html`
|
||||
<p>
|
||||
<uui-button
|
||||
look="secondary"
|
||||
href="${this.settings.reportUrl}"
|
||||
target="_blank"
|
||||
label="View Report"></uui-button>
|
||||
</p>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
<p>Simply click <strong>continue</strong> below to be guided through the rest of the upgrade.</p>
|
||||
|
||||
<form id="authorizeUpgradeForm" @submit=${this._handleSubmit}>
|
||||
<p>
|
||||
<uui-button
|
||||
id="authorizeUpgrade"
|
||||
type="submit"
|
||||
look="primary"
|
||||
color="positive"
|
||||
label="Continue"
|
||||
state=${ifDefined(this.upgrading ? 'waiting' : undefined)}></uui-button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
${this._renderError()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderError() {
|
||||
return html` ${this.errorMessage ? html`<p class="error">${this.errorMessage}</p>` : ''} `;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${this.fetching ? html`<div class="center"><uui-loader></uui-loader></div>` : this._renderLayout()} `;
|
||||
}
|
||||
|
||||
_handleSubmit = async (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('onAuthorizeUpgrade', { detail: e, bubbles: true }));
|
||||
};
|
||||
}
|
||||
|
||||
export default UmbUpgraderView;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-upgrader-view': UmbUpgraderView;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,82 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from 'lit';
|
||||
import '../installer/installer-layout.element';
|
||||
import './upgrader-view.element';
|
||||
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
|
||||
import { UmbContextProviderMixin } from '../core/context';
|
||||
import { getUpgradeSettings, PostUpgradeAuthorize } from '../core/api/fetcher';
|
||||
import { UmbracoUpgrader } from '../core/models';
|
||||
|
||||
/**
|
||||
* @element umb-upgrader
|
||||
*/
|
||||
@customElement('umb-upgrader')
|
||||
export class UmbUpgrader extends UmbContextProviderMixin(LitElement) {
|
||||
static styles: CSSResultGroup = [css``];
|
||||
export class UmbUpgrader extends LitElement {
|
||||
@state()
|
||||
private upgradeSettings?: UmbracoUpgrader;
|
||||
|
||||
@state()
|
||||
step = 1;
|
||||
private fetching = true;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener('next', () => this._handleNext());
|
||||
this.addEventListener('previous', () => this._goToPreviousStep());
|
||||
}
|
||||
@state()
|
||||
private upgrading = false;
|
||||
|
||||
private _handleNext() {
|
||||
this.step++;
|
||||
}
|
||||
@state()
|
||||
private errorMessage = '';
|
||||
|
||||
private _goToPreviousStep() {
|
||||
this.step--;
|
||||
constructor() {
|
||||
super();
|
||||
this._setup();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<h1>Please implement me</h1>`;
|
||||
return html`<umb-installer-layout>
|
||||
<umb-upgrader-view
|
||||
.fetching=${this.fetching}
|
||||
.upgrading=${this.upgrading}
|
||||
.settings=${this.upgradeSettings}
|
||||
.errorMessage=${this.errorMessage}
|
||||
@onAuthorizeUpgrade=${this._handleSubmit}></umb-upgrader-view>
|
||||
</umb-installer-layout>`;
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
this.fetching = true;
|
||||
|
||||
try {
|
||||
const { data } = await getUpgradeSettings({});
|
||||
|
||||
this.upgradeSettings = data;
|
||||
} catch (e) {
|
||||
if (e instanceof getUpgradeSettings.Error) {
|
||||
this.errorMessage = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
this.fetching = false;
|
||||
}
|
||||
|
||||
_handleSubmit = async (e: CustomEvent<SubmitEvent>) => {
|
||||
e.stopPropagation();
|
||||
this.errorMessage = '';
|
||||
this.upgrading = true;
|
||||
|
||||
try {
|
||||
await PostUpgradeAuthorize({});
|
||||
history.pushState(null, '', '/');
|
||||
} catch (e) {
|
||||
if (e instanceof PostUpgradeAuthorize.Error) {
|
||||
const error = e.getActualType();
|
||||
if (error.status === 400) {
|
||||
this.errorMessage = error.data.detail || 'Unknown error, please try again';
|
||||
}
|
||||
} else {
|
||||
this.errorMessage = 'Unknown error, please try again';
|
||||
}
|
||||
}
|
||||
|
||||
this.upgrading = false;
|
||||
};
|
||||
}
|
||||
|
||||
export default UmbUpgrader;
|
||||
|
||||
58
src/Umbraco.Web.UI.Client/src/upgrader/upgrader.stories.ts
Normal file
58
src/Umbraco.Web.UI.Client/src/upgrader/upgrader.stories.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import '.';
|
||||
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import { UmbUpgraderView } from './upgrader-view.element';
|
||||
|
||||
export default {
|
||||
title: 'Components/Upgrader/States',
|
||||
args: {
|
||||
errorMessage: '',
|
||||
upgrading: false,
|
||||
fetching: false,
|
||||
settings: {
|
||||
currentState: '2b20c6e7',
|
||||
newState: '2b20c6e8',
|
||||
oldVersion: '12.0.0',
|
||||
newVersion: '13.0.0',
|
||||
reportUrl: 'https://our.umbraco.com/download/releases/1000',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
actions: {
|
||||
handles: ['onAuthorizeUpgrade'],
|
||||
},
|
||||
},
|
||||
decorators: [
|
||||
(story) =>
|
||||
html`<div
|
||||
style="margin:2rem; max-width:400px;border:1px solid #ccc;border-radius:30px 0px 0px 30px;padding:var(--uui-size-layout-4) var(--uui-size-layout-4) var(--uui-size-layout-2) var(--uui-size-layout-4);">
|
||||
${story()}
|
||||
</div>`,
|
||||
],
|
||||
} as Meta<UmbUpgraderView>;
|
||||
|
||||
const Template: Story<UmbUpgraderView> = ({ upgrading, errorMessage, settings, fetching }) =>
|
||||
html`<umb-upgrader-view
|
||||
.upgrading=${upgrading}
|
||||
.errorMessage=${errorMessage}
|
||||
.settings=${settings}
|
||||
.fetching=${fetching}></umb-upgrader-view>`;
|
||||
|
||||
export const Overview = Template.bind({});
|
||||
|
||||
export const Upgrading = Template.bind({});
|
||||
Upgrading.args = {
|
||||
upgrading: true,
|
||||
};
|
||||
|
||||
export const Fetching = Template.bind({});
|
||||
Fetching.args = {
|
||||
fetching: true,
|
||||
};
|
||||
|
||||
export const Error = Template.bind({});
|
||||
Error.args = {
|
||||
errorMessage: 'Something went wrong',
|
||||
};
|
||||
19
src/Umbraco.Web.UI.Client/src/upgrader/upgrader.test.ts
Normal file
19
src/Umbraco.Web.UI.Client/src/upgrader/upgrader.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import { UmbUpgrader } from './upgrader.element';
|
||||
|
||||
describe('UmbUpgrader', () => {
|
||||
let element: UmbUpgrader;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-upgrader></umb-upgrader>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', () => {
|
||||
expect(element).to.be.instanceOf(UmbUpgrader);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', () => {
|
||||
expect(element).shadowDom.to.be.accessible();
|
||||
});
|
||||
});
|
||||
2
src/Umbraco.Web.UI.Client/src/vite-env.d.ts
vendored
2
src/Umbraco.Web.UI.Client/src/vite-env.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
VITE_UMBRACO_INSTALL_STATUS: string;
|
||||
VITE_UMBRACO_INSTALL_STATUS: 'running' | 'must-upgrade' | 'must-install';
|
||||
VITE_UMBRACO_INSTALL_PRECONFIGURED: string;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import './installer';
|
||||
import './server';
|
||||
import './upgrader';
|
||||
import './user';
|
||||
|
||||
import { api } from '@airtasker/spot';
|
||||
|
||||
38
src/Umbraco.Web.UI.Client/temp-schema-generator/upgrader.ts
Normal file
38
src/Umbraco.Web.UI.Client/temp-schema-generator/upgrader.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { body, defaultResponse, endpoint, request, response } from '@airtasker/spot';
|
||||
|
||||
import { ProblemDetails } from './models';
|
||||
|
||||
@endpoint({
|
||||
method: 'GET',
|
||||
path: '/upgrade/settings',
|
||||
})
|
||||
export class GetUpgradeSettings {
|
||||
@response({ status: 200 })
|
||||
success(@body body: UpgradeSettingsResponse) {}
|
||||
|
||||
@defaultResponse
|
||||
default(@body body: ProblemDetails) {}
|
||||
}
|
||||
|
||||
@endpoint({
|
||||
method: 'POST',
|
||||
path: '/upgrade/authorize',
|
||||
})
|
||||
export class PostUpgradeAuthorize {
|
||||
@request
|
||||
request() {}
|
||||
|
||||
@response({ status: 201 })
|
||||
success() {}
|
||||
|
||||
@response({ status: 400 })
|
||||
badRequest(@body body: ProblemDetails) {}
|
||||
}
|
||||
|
||||
export interface UpgradeSettingsResponse {
|
||||
currentState: string;
|
||||
newState: string;
|
||||
newVersion: string;
|
||||
oldVersion: string;
|
||||
reportUrl: string;
|
||||
}
|
||||
Reference in New Issue
Block a user