diff --git a/src/Umbraco.Web.UI.Client/devops/localization/compare-languages.js b/src/Umbraco.Web.UI.Client/devops/localization/compare-languages.js new file mode 100644 index 0000000000..9a532f300f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/devops/localization/compare-languages.js @@ -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; +} diff --git a/src/Umbraco.Web.UI.Client/devops/localization/unused-language-keys.js b/src/Umbraco.Web.UI.Client/devops/localization/unused-language-keys.js new file mode 100644 index 0000000000..6fa81fc45e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/devops/localization/unused-language-keys.js @@ -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`); + +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; +} diff --git a/src/Umbraco.Web.UI.Client/devops/tsconfig/index.js b/src/Umbraco.Web.UI.Client/devops/tsconfig/index.js index e05b6550ab..e7dea16f82 100644 --- a/src/Umbraco.Web.UI.Client/devops/tsconfig/index.js +++ b/src/Umbraco.Web.UI.Client/devops/tsconfig/index.js @@ -3,13 +3,23 @@ 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\n\n`; +const tsconfigComment = ` +/* ------------------------------------------------------------------------- + + +DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js + + + + +--------------------------------------------------------------------------- */ +`; const tsConfigBase = { compilerOptions: { module: 'esnext', - target: 'ES2020', - lib: ['es2020', 'dom', 'dom.iterable'], + target: 'es2022', + lib: ['es2022', 'dom', 'dom.iterable'], outDir: './types', allowSyntheticDefaultImports: true, experimentalDecorators: true, diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts index ef690d37fc..fd54f80ec6 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts @@ -60,36 +60,25 @@ export class UmbAppAuthController extends UmbControllerBase { throw new Error('[Fatal] No auth providers available'); } - // If the user is timed out, we can show the login modal directly - if (userLoginState === 'timedOut') { - const selected = await this.#showLoginModal(userLoginState); - - if (!selected) { - return false; + // If we are logging in, we need to check if we can redirect directly to the provider + if (userLoginState === 'loggingIn') { + // One provider available (most likely the Umbraco provider), so initiate the authorization request to the default provider + if (availableProviders.length === 1) { + await this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName, true); + return this.#updateState(); } - return this.#updateState(); - } + // Check if any provider is redirecting directly to the provider + const redirectProvider = availableProviders.find((provider) => provider.meta?.behavior?.autoRedirect); - if (availableProviders.length === 1) { - // One provider available (most likely the Umbraco provider), so initiate the authorization request to the default provider - await this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName, true); - return this.#updateState(); - } - - // Check if any provider is redirecting directly to the provider - const redirectProvider = - userLoginState === 'loggingIn' - ? availableProviders.find((provider) => provider.meta?.behavior?.autoRedirect) - : undefined; - - if (redirectProvider) { // Redirect directly to the provider - await this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName, true); - return this.#updateState(); + if (redirectProvider) { + await this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName, true); + return this.#updateState(); + } } - // Show the provider selection screen + // Otherwise we can show the provider selection screen directly, because the user is either logged out, timed out, or has more than one provider available const selected = await this.#showLoginModal(userLoginState); if (!selected) { diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.context.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.context.ts index 76940d7d5a..f460ac4776 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.context.ts @@ -8,6 +8,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registr import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; export class UmbBackofficeContext extends UmbContextBase { #activeSectionAlias = new UmbStringState(undefined); @@ -26,7 +27,13 @@ export class UmbBackofficeContext extends UmbContextBase { this.#allowedSections.setValue([...sections]); }); - this.#getVersion(); + // TODO: We need to ensure this request is called every time the user logs in, but this should be done somewhere across the app and not here [JOV] + this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => { + this.observe(authContext.isAuthorized, (isAuthorized) => { + if (!isAuthorized) return; + this.#getVersion(); + }); + }); } async #getVersion() { diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts index 179676c253..d292fc9be4 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts @@ -7,6 +7,7 @@ import { } from '@umbraco-cms/backoffice/extension-registry'; import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; import './components/index.js'; @@ -58,7 +59,15 @@ export class UmbBackofficeElement extends UmbLitElement { umbExtensionsRegistry.registerMany(packageModule.extensions); }); - new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions(); + const serverExtensions = new UmbServerExtensionRegistrator(this, umbExtensionsRegistry); + + // TODO: We need to ensure this request is called every time the user logs in, but this should be done somewhere across the app and not here [JOV] + this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => { + this.observe(authContext.isAuthorized, (isAuthorized) => { + if (!isAuthorized) return; + serverExtensions.registerPrivateExtensions(); + }); + }); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts index 0347450b50..45c43ac734 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/bs.ts @@ -466,8 +466,8 @@ export default { confirmlogout: 'Jeste li sigurni?', confirmSure: 'Jeste li sigurni?', cut: 'Izreži', - editdictionary: 'Uredi stavku iz rječnika', - editlanguage: 'Uredi jezik', + editDictionary: 'Uredi stavku iz rječnika', + editLanguage: 'Uredi jezik', editSelectedMedia: 'Uredite odabrane medije', insertAnchor: 'Umetnite lokalnu vezu', insertCharacter: 'Umetni znak', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts index a77591898a..f26e8f1a23 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/cs-cz.ts @@ -413,8 +413,8 @@ export default { confirmlogout: 'Jste si jistí?', confirmSure: 'Jste si jistí?', cut: 'Vyjmout', - editdictionary: 'Editovat položku slovníku', - editlanguage: 'Editovat jazyk', + editDictionary: 'Editovat položku slovníku', + editLanguage: 'Editovat jazyk', editSelectedMedia: 'Edit selected media', insertAnchor: 'Vložit místní odkaz', insertCharacter: 'Vložit znak', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index be0653c054..b2dcc78cd8 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -62,7 +62,6 @@ export default { unlock: 'Lås op', createblueprint: 'Opret indholdsskabelon', resendInvite: 'Gensend Invitation', - defaultValue: 'Standard værdi', editContent: 'Edit content', chooseWhereToImport: 'Choose where to import', }, @@ -180,7 +179,6 @@ export default { rollback: 'Brugeren har tilbagerullet indholdet til en tidligere tilstand', sendtopublish: 'Brugeren har sendt indholdet til udgivelse', sendtopublishvariant: 'Brugeren har sendt indholdet til udgivelse for sprogene: %0%', - sendtotranslate: 'Brugeren har sendt indholdet til oversættelse', sort: 'Brugeren har sorteret de underliggende sider', custom: '%0%', smallCopy: 'Kopieret', @@ -195,7 +193,6 @@ export default { smallRollBack: 'Indhold tilbagerullet', smallSendToPublish: 'Sendt til udgivelse', smallSendToPublishVariant: 'Sendt til udgivelse', - smallSendToTranslate: 'Sendt til oversættelse', smallSort: 'Sorteret', smallCustom: 'Brugerdefineret', historyIncludingVariants: 'Historik (alle sprog)', @@ -473,7 +470,6 @@ export default { urlLinkPicker: 'Link', anchorLinkPicker: 'Lokalt link / querystreng', anchorInsert: 'Navn på lokalt link', - assignDomain: 'Rediger domæner', closeThisWindow: 'Luk denne dialog', confirmdelete: 'Er du sikker på at du vil slette', confirmdisable: 'Er du sikker på du vil deaktivere', @@ -482,8 +478,8 @@ export default { confirmlogout: 'Er du sikker på at du vil forlade Umbraco?', confirmSure: 'Er du sikker?', cut: 'Klip', - editdictionary: 'Rediger ordbogsnøgle', - editlanguage: 'Rediger sprog', + editDictionary: 'Rediger ordbogsnøgle', + editLanguage: 'Rediger sprog', editSelectedMedia: 'Rediger det valgte medie', insertAnchor: 'Indsæt lokalt link', insertCharacter: 'Indsæt tegn', @@ -592,7 +588,7 @@ export default { examineManagement: { configuredSearchers: 'Konfigurerede søgere', configuredSearchersDescription: - 'Viser egenskaber og værktøjer til enhver konfigureret søger (dvs. som en\n multi-indekssøger)\n ', + 'Viser egenskaber og værktøjer til enhver konfigureret søger (dvs. som en multi-indekssøger)', fieldValues: 'Feltværdier', healthStatus: 'Sundhedstilstand', healthStatusDescription: 'Indeksets sundhedstilstand, og hvis det kan læses', @@ -601,10 +597,10 @@ export default { indexInfoDescription: 'Viser indeksets egenskaber', manageIndexes: 'Administrer Examine indekserne', manageIndexesDescription: - 'Giver dig mulighed for at se detaljerne for hvert indeks og giver nogle\n værktøjer til styring af indeksørerne\n ', + 'Giver dig mulighed for at se detaljerne for hvert indeks og giver nogle værktøjer til styring af indeksørerne', rebuildIndex: 'Genopbyg indeks', rebuildIndexWarning: - '\n Dette vil medføre, at indekset genopbygges.
\n Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.
\n Det anbefales ikke at genopbygge et indeks i perioder med høj websitetrafik eller når redaktører redigerer indhold.\n ', + 'Dette vil medføre, at indekset genopbygges.
Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.
Det anbefales ikke at genopbygge et indeks i perioder med høj websitetrafik eller når redaktører redigerer indhold.', searchers: 'Søgere', searchDescription: 'Søg i indekset og se resultaterne', tools: 'Værktøjer', @@ -612,7 +608,7 @@ export default { fields: 'felter', indexCannotRead: 'Indexet skal bygges igen, for at kunne læses', processIsTakingLonger: - 'Processen tager længere tid end forventet. Kontrollér Umbraco loggen for at se om\n der er sket fejl under operationen\n ', + 'Processen tager længere tid end forventet. Kontrollér Umbraco loggen for at se om der er sket fejl under operationen', indexCannotRebuild: 'Dette index kan ikke genbygges for det ikke har nogen', iIndexPopulator: 'IIndexPopulator', contentInIndex: 'Content in index', @@ -1273,18 +1269,13 @@ export default { }, sections: { content: 'Indhold', - developer: 'Udvikler', - installer: 'Umbraco konfigurationsguide', media: 'Mediearkiv', member: 'Medlemmer', packages: 'Pakker', marketplace: 'Marketplace', - newsletters: 'Nyhedsbreve', settings: 'Indstillinger', - statistics: 'Statistik', translation: 'Oversættelse', users: 'Brugere', - help: 'Hjælp', }, help: { tours: 'Tours', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts index 562a14bd64..50fbf86afe 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/de-de.ts @@ -487,8 +487,8 @@ export default { confirmlogout: 'Sind Sie sich wirklich abmelden?', confirmSure: 'Sind Sie sicher?', cut: 'Ausschneiden', - editdictionary: 'Wörterbucheintrag bearbeiten', - editlanguage: 'Sprache bearbeiten', + editDictionary: 'Wörterbucheintrag bearbeiten', + editLanguage: 'Sprache bearbeiten', editSelectedMedia: 'Ausgewähltes Medien', insertAnchor: 'Anker einfügen', insertCharacter: 'Zeichen einfügen', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 463bd30a42..7a039d433e 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -488,8 +488,8 @@ export default { confirmlogout: 'Are you sure?', confirmSure: 'Are you sure?', cut: 'Cut', - editdictionary: 'Edit Dictionary Item', - editlanguage: 'Edit Language', + editDictionary: 'Edit Dictionary Item', + editLanguage: 'Edit Language', editSelectedMedia: 'Edit selected media', insertAnchor: 'Insert local link', insertCharacter: 'Insert character', @@ -597,7 +597,7 @@ export default { examineManagement: { configuredSearchers: 'Configured Searchers', configuredSearchersDescription: - 'Shows properties and tools for any configured Searcher (i.e. such as a\n multi-index searcher)\n ', + 'Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)', fieldValues: 'Field values', healthStatus: 'Health status', healthStatusDescription: 'The health status of the index and if it can be read', @@ -607,10 +607,10 @@ export default { indexInfoDescription: 'Lists the properties of the index', manageIndexes: "Manage Examine's indexes", manageIndexesDescription: - 'Allows you to view the details of each index and provides some tools for\n managing the indexes\n ', + 'Allows you to view the details of each index and provides some tools for managing the indexes', rebuildIndex: 'Rebuild index', rebuildIndexWarning: - '\n This will cause the index to be rebuilt.
\n Depending on how much content there is in your site this could take a while.
\n It is not recommended to rebuild an index during times of high website traffic or when editors are editing content.\n ', + 'This will cause the index to be rebuilt.
Depending on how much content there is in your site this could take a while.
It is not recommended to rebuild an index during times of high website traffic or when editors are editing content.', searchers: 'Searchers', searchDescription: 'Search the index and view the results', tools: 'Tools', @@ -618,7 +618,7 @@ export default { fields: 'fields', indexCannotRead: 'The index cannot be read and will need to be rebuilt', processIsTakingLonger: - 'The process is taking longer than expected, check the Umbraco log to see if there\n have been any errors during this operation\n ', + 'The process is taking longer than expected, check the Umbraco log to see if there have been any errors during this operation', indexCannotRebuild: 'This index cannot be rebuilt because it has no assigned', iIndexPopulator: 'IIndexPopulator', }, @@ -2307,8 +2307,7 @@ export default { documentationHeader: 'Documentation', documentationDescription: 'Read more about working with the items in Settings in our Documentation.', communityHeader: 'Community', - communitytDescription: 'Ask a question in the community forum or our Discord community.', - + communityDescription: 'Ask a question in the community forum or our Discord community.', trainingHeader: 'Training', trainingDescription: 'Find out about real-life training and certification opportunities', supportHeader: 'Support', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index dd25b68ca6..388571673a 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -641,6 +641,7 @@ export default { entername: 'Enter a name...', enteremail: 'Enter an email...', enterusername: 'Enter a username...', + enterdate: 'Set a date...', label: 'Label...', enterDescription: 'Enter a description...', search: 'Type to search...', @@ -1054,34 +1055,9 @@ export default { greeting4: 'Welcome', greeting5: 'Welcome', greeting6: 'Welcome', - instruction: 'Log in below', - signInWith: 'Sign in with', - timeout: 'Session timed out', - userFailedLogin: "Oops! We couldn't log you in. Please check your credentials and try again.", - bottomText: - '

© 2001 - %0%
Umbraco.com

', - forgottenPassword: 'Forgotten password?', - forgottenPasswordInstruction: - 'An email will be sent to the address specified with a link to reset your\n password\n ', - requestPasswordResetConfirmation: - 'An email with password reset instructions will be sent to the\n specified address if it matched our records\n ', - showPassword: 'Show password', - hidePassword: 'Hide password', - returnToLogin: 'Return to login form', - setPasswordInstruction: 'Please provide a new password', - setPasswordConfirmation: 'Your Password has been updated', - resetCodeExpired: 'The link you have clicked on is invalid or has expired', - resetPasswordEmailCopySubject: 'Umbraco: Reset Password', - resetPasswordEmailCopyFormat: - "\n \n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\n
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\n\n
\n\t\t\t\t\t\t\t\t\t\t\n
\n\n
\n
\n\n\n\n\n\n\n
\n
\n
\n\n\n\n\n
\n\n\n\n\n
\n

\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tPassword reset requested\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t

\n

\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tYour username to login to the Umbraco backoffice is: %0%\n

\n

\n\n\n\n\n\n\n
\n\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tClick this link to reset your password\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n
\n

\n

If you cannot click on the link, copy and paste this URL into your browser window:

\n\n\n\n\n
\n\n%1%\n\n
\n

\n
\n
\n


\n
\n
\n\n\n\t", - mfaSecurityCodeSubject: 'Umbraco: Security Code', - mfaSecurityCodeMessage: 'Your security code is: %0%', - '2faTitle': 'One last step', - '2faText': 'You have enabled 2-factor authentication and must verify your identity.', - '2faMultipleText': 'Please choose a 2-factor provider', - '2faCodeInput': 'Verification code', - '2faCodeInputHelp': 'Please enter the verification code', - '2faInvalidCode': 'Invalid code entered', + instruction: 'Sign in to Umbraco', + signInWith: 'Sign in with {0}', + timeout: 'Your session has timed out. Please sign in again below.', }, main: { dashboard: 'Dashboard', @@ -1107,13 +1083,6 @@ export default { notifications: { editNotifications: 'Select your notification for %0%', notificationsSavedFor: 'Notification settings saved for', - mailBody: - "\n Hi %0%\n\n This is an automated mail to inform you that the task '%1%'\n has been performed on the page '%2%'\n by the user '%3%'\n\n Go to http://%4%/#/content/content/edit/%5% to edit.\n\n %6%\n\n Have a nice day!\n Cheers from the Umbraco robot\n ", - mailBodyVariantSummary: 'The following languages have been modified %0%', - mailBodyHtml: - "\n \n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\n
\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\n\n\n
\n\t\t\t\t\t\t\t\t\t\t\n
\n\n
\n
\n\n\n\n\n\n\n
\n
\n
\n\n\n\n\n
\n\n\n\n\n
\n

\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tHi %0%,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t

\n

\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tThis is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%'\n

\n\n\n\n\n\n\n
\n\n
\nEDIT
\n
\n

\n

Update summary:

\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t%6%\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t

\n

\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tHave a nice day!

\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tCheers from the Umbraco robot\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t

\n
\n
\n


\n
\n
\n\n\n\t", - mailBodyVariantHtmlSummary: '

The following languages have been modified:

\n %0%\n ', - mailSubject: '[%0%] Notification about %1% performed on %2%', notifications: 'Notifications', }, packager: { @@ -1384,6 +1353,7 @@ export default { folderUploadNotAllowed: 'This file is being uploaded as part of a folder, but creating a new folder is not allowed here', folderCreationNotAllowed: 'Creating a new folder is not allowed here', + contentPublishedFailedByEvent: 'Content could not be published, a 3rd party add-in cancelled the action', contentTypeDublicatePropertyType: 'Property type already exists', contentTypePropertyTypeCreated: 'Property type created', contentTypePropertyTypeCreatedText: 'Name: %0%
DataType: %1%', @@ -1397,6 +1367,7 @@ export default { cssSavedText: 'Stylesheet saved without any errors', dataTypeSaved: 'Datatype saved', dictionaryItemSaved: 'Dictionary item saved', + editContentPublishedFailedByParent: 'Content could not be published, because a parent page is not published', editContentPublishedHeader: 'Content published', editContentPublishedText: 'and visible on the website', editBlueprintSavedHeader: 'Content Template saved', @@ -1523,6 +1494,7 @@ export default { insertPartialViewDesc: "\n A partial view is a separate template file which can be rendered inside another\n template, it's great for reusing markup or for separating complex templates into separate files.\n ", mastertemplate: 'Master template', + quickGuide: 'Quick guide to template tags', noMaster: 'No master', renderBody: 'Render child template', renderBodyDesc: @@ -1815,6 +1787,8 @@ export default { preContent: 'Insert before field', recursive: 'Recursive', recursiveDescr: 'Yes, make it recursive', + removeParagraph: 'Remove paragraph tags', + removeParagraphHelp: 'Will remove paragraph tags from the field value', standardFields: 'Standard Fields', uppercase: 'Uppercase', urlEncode: 'URL encode', @@ -1828,8 +1802,6 @@ export default { DownloadXmlDTD: 'Download XML DTD', fields: 'Fields', includeSubpages: 'Include subpages', - mailBody: - "\n Hi %0%\n\n This is an automated mail to inform you that the document '%1%'\n has been requested for translation into '%5%' by %2%.\n\n Go to http://%3%/translation/details.aspx?id=%4% to edit.\n\n Or log into Umbraco to get an overview of your translation tasks\n http://%3%\n\n Have a nice day!\n Cheers from the Umbraco robot\n ", noTranslators: 'No translator users found. Please create a translator user before you start sending\n content to translation\n ', pageHasBeenSendToTranslation: "The page '%0%' has been send to translation", @@ -1878,6 +1850,8 @@ export default { stylesheets: 'Stylesheets', templates: 'Templates', logViewer: 'Log Viewer', + userPermissions: 'User permissions', + userTypes: 'User types', users: 'Users', settingsGroup: 'Settings', templatingGroup: 'Templating', @@ -2274,6 +2248,9 @@ export default { labelUsedByMediaTypes: 'Used in Media Types', labelUsedByMemberTypes: 'Used in Member Types', usedByProperties: 'Used by', + labelUsedByDocuments: 'Used in Documents', + labelUsedByMembers: 'Used in Members', + labelUsedByMedia: 'Used in Media', labelUsedItems: 'Items in use', labelUsedDescendants: 'Descendants in use', deleteWarning: 'This item or its descendants is being used. Deletion can lead to broken links on your website.', @@ -2284,6 +2261,10 @@ export default { labelUsedByItems: 'Referenced by the following items', labelDependsOnThis: 'The following items depend on this', labelDependentDescendants: 'The following descending items have dependencies', + labelMoreReferences: (count: number) => { + if (count === 1) return '...and one more item'; + return `...and ${count} more items`; + }, }, logViewer: { deleteSavedSearch: 'Delete Saved Search', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts index bff4a9e8b0..5903b96f02 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/es-es.ts @@ -288,8 +288,8 @@ export default { confirmlogout: '¿Estás seguro?', confirmSure: '¿Estás seguro?', cut: 'Cortar', - editdictionary: 'Editar entrada del Diccionario', - editlanguage: 'Editar idioma', + editDictionary: 'Editar entrada del Diccionario', + editLanguage: 'Editar idioma', insertAnchor: 'Agregar enlace interno', insertCharacter: 'Insertar carácter', insertgraphicheadline: 'Insertar titular gráfico', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts index 8293de4ae3..05922b01d7 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/fr-fr.ts @@ -426,8 +426,8 @@ export default { confirmlogout: 'Êtes-vous certain(e)?', confirmSure: 'Êtes-vous certain(e)?', cut: 'Couper', - editdictionary: 'Editer une entrée du Dictionnaire', - editlanguage: 'Modifier la langue', + editDictionary: 'Editer une entrée du Dictionnaire', + editLanguage: 'Modifier la langue', editSelectedMedia: 'Modifier le media sélectionné', insertAnchor: 'Insérer un lien local (ancre)', insertCharacter: 'Insérer un caractère', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts index 718fa1f37f..9a4b9ca146 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/he-il.ts @@ -166,8 +166,8 @@ export default { confirmlogout: 'האם הינך בטוח?', confirmSure: 'האם אתה בטוח?', cut: 'גזור', - editdictionary: 'ערוך פרט מילון', - editlanguage: 'ערוך שפה', + editDictionary: 'ערוך פרט מילון', + editLanguage: 'ערוך שפה', insertAnchor: 'הוסף קישור מקומי', insertCharacter: 'הוסף תו', insertgraphicheadline: 'הוסף פס גרפי', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts index 8ded9d8f8f..96ecf0829c 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/hr-hr.ts @@ -467,8 +467,8 @@ export default { confirmlogout: 'Jeste li sigurni?', confirmSure: 'Jeste li sigurni?', cut: 'Izreži', - editdictionary: 'Uredi stavku iz rječnika', - editlanguage: 'Uredi jezik', + editDictionary: 'Uredi stavku iz rječnika', + editLanguage: 'Uredi jezik', editSelectedMedia: 'Uredite odabrane medije', editWebhook: 'Uredi webhook', insertAnchor: 'Umetni lokalnu vezu', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts index a75880201f..49bff1103b 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/it-it.ts @@ -479,8 +479,8 @@ export default { confirmlogout: 'Sei sicuro?', confirmSure: 'Sei sicuro?', cut: 'Taglia', - editdictionary: 'Modifica elemento del Dizionario', - editlanguage: 'Modifica la lingua', + editDictionary: 'Modifica elemento del Dizionario', + editLanguage: 'Modifica la lingua', editSelectedMedia: 'Modifica il media selezionato', insertAnchor: 'Inserisci il link locale', insertCharacter: 'Inserisci carattere', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts index 9a362f5583..aa44887bda 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ja-jp.ts @@ -215,8 +215,8 @@ export default { confirmlogout: 'ログアウトしますか?', confirmSure: '本当にいいですか?', cut: '切り取り', - editdictionary: 'ディクショナリのアイテムの編集', - editlanguage: '言語の編集', + editDictionary: 'ディクショナリのアイテムの編集', + editLanguage: '言語の編集', insertAnchor: 'アンカーの挿入', insertCharacter: '文字の挿入', insertgraphicheadline: 'ヘッドライン画像の挿入', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts index 16d747ed2f..e1804fb584 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ko-kr.ts @@ -166,8 +166,8 @@ export default { confirmlogout: '로그아웃 하시겠습니까?', confirmSure: '확실합니까?', cut: "TRANSLATE ME: 'Cut'", - editdictionary: '사전 항목 편집', - editlanguage: '언어 편집', + editDictionary: '사전 항목 편집', + editLanguage: '언어 편집', insertAnchor: '내부 링크삽입', insertCharacter: '문자열 삽입', insertgraphicheadline: '그래픽 헤드라인 삽입', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts index 594ce944cd..062e0e4ca2 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/nb-no.ts @@ -210,8 +210,8 @@ export default { confirmlogout: 'Er du sikker på at du vil forlate Umbraco?', confirmSure: 'Er du sikker?', cut: 'Klipp ut', - editdictionary: 'Rediger ordboksnøkkel', - editlanguage: 'Rediger språk', + editDictionary: 'Rediger ordboksnøkkel', + editLanguage: 'Rediger språk', insertAnchor: 'Sett inn lokal link', insertCharacter: 'Sett inn spesialtegn', insertgraphicheadline: 'Sett inn grafisk overskrift', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts index e0b9340c35..30f0e6d4df 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/nl-nl.ts @@ -436,8 +436,8 @@ export default { confirmlogout: 'Weet je het zeker?', confirmSure: 'Weet je het zeker?', cut: 'Knippen', - editdictionary: 'Pas woordenboekitem aan', - editlanguage: 'Taal aanpassen', + editDictionary: 'Pas woordenboekitem aan', + editLanguage: 'Taal aanpassen', editSelectedMedia: 'Geselecteerde media bewerken', insertAnchor: 'Lokale link invoegen', insertCharacter: 'Karakter invoegen', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts index 427f15b420..edbcbeef73 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pl-pl.ts @@ -290,8 +290,8 @@ export default { confirmlogout: 'Jesteś pewny?', confirmSure: 'Jesteś pewny?', cut: 'Wytnij', - editdictionary: 'Edytuj element słownika', - editlanguage: 'Edytuj język', + editDictionary: 'Edytuj element słownika', + editLanguage: 'Edytuj język', insertAnchor: 'Wstaw link wewnętrzny', insertCharacter: 'Wstaw znak', insertgraphicheadline: 'Wstaw graficzny nagłówek', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts index 5f5e9a36e2..fdfec8369d 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt-br.ts @@ -166,8 +166,8 @@ export default { confirmlogout: 'Tem certeza', confirmSure: 'Tem certeza?', cut: 'Cortar', - editdictionary: 'Editar Item de Dicionário', - editlanguage: 'Editar Linguagem', + editDictionary: 'Editar Item de Dicionário', + editLanguage: 'Editar Linguagem', insertAnchor: 'Inserir link local', insertCharacter: 'Inserir charactere', insertgraphicheadline: 'Inserir manchete de gráfico', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts index f0048a4594..90ec2b31db 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/ru-ru.ts @@ -347,8 +347,8 @@ export default { confirmlogout: 'Вы уверены?', confirmSure: 'Вы уверены?', cut: 'Вырезать', - editdictionary: 'Править статью словаря', - editlanguage: 'Изменить язык', + editDictionary: 'Править статью словаря', + editLanguage: 'Изменить язык', insertAnchor: 'Вставить локальную ссылку (якорь)', insertCharacter: 'Вставить символ', insertgraphicheadline: 'Вставить графический заголовок', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/sv-se.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/sv-se.ts index 530bad4526..a938ef0921 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/sv-se.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/sv-se.ts @@ -360,8 +360,8 @@ export default { confirmlogout: 'Är du säker?', confirmSure: 'Är du säker?', cut: 'Klipp ut', - editdictionary: 'Redigera ord i ordboken', - editlanguage: 'Redigera språk', + editDictionary: 'Redigera ord i ordboken', + editLanguage: 'Redigera språk', insertAnchor: 'Infoga ankarlänk', insertCharacter: 'Infoga tecken', insertgraphicheadline: 'Infoga grafisk rubrik', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts index b90032dd41..ee92d31390 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/tr-tr.ts @@ -421,8 +421,8 @@ export default { confirmlogout: 'Emin misiniz?', confirmSure: 'Emin misiniz?', cut: 'Kes', - editdictionary: 'Sözlük Öğesini Düzenle', - editlanguage: 'Dili Düzenle', + editDictionary: 'Sözlük Öğesini Düzenle', + editLanguage: 'Dili Düzenle', editSelectedMedia: 'Seçili medyayı düzenle', insertAnchor: 'Yerel bağlantı ekle', insertCharacter: 'Karakter ekle', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts index 2b04c4c92a..db7da8e88f 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/uk-ua.ts @@ -346,8 +346,8 @@ export default { confirmlogout: 'Ви впевнені?', confirmSure: 'Ви впевнені?', cut: 'Вирізати', - editdictionary: 'Редагувати статтю словника', - editlanguage: 'Змінити мову', + editDictionary: 'Редагувати статтю словника', + editLanguage: 'Змінити мову', insertAnchor: 'Вставити локальне посилання (якір)', insertCharacter: 'Вставити символ', insertgraphicheadline: 'Вставити графічний заголовок', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts index 58e533df5c..cfdb1c5b48 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-cn.ts @@ -221,8 +221,8 @@ export default { confirmlogout: '您确定吗?', confirmSure: '您确定吗?', cut: '剪切', - editdictionary: '编辑字典项', - editlanguage: '编辑语言', + editDictionary: '编辑字典项', + editLanguage: '编辑语言', insertAnchor: '插入本地链接', insertCharacter: '插入字符', insertgraphicheadline: '插入图片标题', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts index 53418d141d..539ccca2cd 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/zh-tw.ts @@ -220,8 +220,8 @@ export default { confirmlogout: '您確定嗎?', confirmSure: '您確定嗎?', cut: '剪切', - editdictionary: '編輯字典項', - editlanguage: '編輯語言', + editDictionary: '編輯字典項', + editLanguage: '編輯語言', insertAnchor: '插入本地連結', insertCharacter: '插入字元', insertgraphicheadline: '插入圖片標題', diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts index 9b65a819dc..da4e84c5d8 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts @@ -979,6 +979,11 @@ export enum HealthStatusModel { REBUILDING = 'Rebuilding' } +export type HealthStatusResponseModel = { + status: HealthStatusModel +message?: string | null + }; + export type HelpPageResponseModel = { name?: string | null description?: string | null @@ -993,7 +998,7 @@ parent?: ReferenceByIdModel | null export type IndexResponseModel = { name: string -healthStatus: HealthStatusModel +healthStatus: HealthStatusResponseModel canRebuild: boolean searcherName: string documentCount: number @@ -5236,15 +5241,15 @@ PostWebhook: { GetWebhookById: { id: string + }; +DeleteWebhookById: { + id: string + }; PutWebhookById: { id: string requestBody?: UpdateWebhookRequestModel - }; -DeleteWebhookById: { - id: string - }; GetWebhookEvents: { skip?: number @@ -5259,8 +5264,8 @@ take?: number ,GetWebhook: PagedWebhookResponseModel ,PostWebhook: string ,GetWebhookById: WebhookResponseModel - ,PutWebhookById: string ,DeleteWebhookById: string + ,PutWebhookById: string ,GetWebhookEvents: PagedWebhookEventModel } diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts index 503eeca574..84d88c42c7 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts @@ -9022,20 +9022,17 @@ take * @returns string Success * @throws ApiError */ - public static putWebhookById(data: WebhookData['payloads']['PutWebhookById']): CancelablePromise { + public static deleteWebhookById(data: WebhookData['payloads']['DeleteWebhookById']): CancelablePromise { const { - id, -requestBody + id } = data; return __request(OpenAPI, { - method: 'PUT', + method: 'DELETE', url: '/umbraco/management/api/v1/webhook/{id}', path: { id }, - body: requestBody, - mediaType: 'application/json', responseHeader: 'Umb-Notifications', errors: { 400: `Bad Request`, @@ -9050,17 +9047,20 @@ requestBody * @returns string Success * @throws ApiError */ - public static deleteWebhookById(data: WebhookData['payloads']['DeleteWebhookById']): CancelablePromise { + public static putWebhookById(data: WebhookData['payloads']['PutWebhookById']): CancelablePromise { const { - id + id, +requestBody } = data; return __request(OpenAPI, { - method: 'DELETE', + method: 'PUT', url: '/umbraco/management/api/v1/webhook/{id}', path: { id }, + body: requestBody, + mediaType: 'application/json', responseHeader: 'Umb-Notifications', errors: { 400: `Bad Request`, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index c794c4a871..509838185f 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -236,7 +236,7 @@ export const data: Array = [ id: 'dt-multiNodeTreePicker', parent: null, editorAlias: 'Umbraco.MultiNodeTreePicker', - editorUiAlias: 'Umb.PropertyEditorUi.TreePicker', + editorUiAlias: 'Umb.PropertyEditorUi.ContentPicker', hasChildren: false, isFolder: false, isDeletable: true, @@ -988,7 +988,7 @@ export const data: Array = [ id: 'dt-integer', parent: null, editorAlias: 'Umbraco.Integer', - editorUiAlias: 'Umb.PropertyEditorUi.Integer', + editorUiAlias: 'Umb.PropertyEditorUi.Number', hasChildren: false, isFolder: false, isDeletable: true, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/examine.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/examine.data.ts index 297384f0de..75124653fe 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/examine.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/examine.data.ts @@ -20,7 +20,7 @@ export const Indexers: IndexResponseModel[] = [ { name: 'ExternalIndex', canRebuild: true, - healthStatus: HealthStatusModel.HEALTHY, + healthStatus: { status: HealthStatusModel.HEALTHY }, documentCount: 0, fieldCount: 0, searcherName: '', @@ -40,7 +40,7 @@ export const Indexers: IndexResponseModel[] = [ { name: 'InternalIndex', canRebuild: true, - healthStatus: HealthStatusModel.HEALTHY, + healthStatus: { status: HealthStatusModel.HEALTHY }, documentCount: 0, fieldCount: 0, searcherName: '', @@ -60,7 +60,7 @@ export const Indexers: IndexResponseModel[] = [ { name: 'MemberIndex', canRebuild: true, - healthStatus: HealthStatusModel.HEALTHY, + healthStatus: { status: HealthStatusModel.HEALTHY }, fieldCount: 0, documentCount: 0, searcherName: '', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts index 16719a60b6..671bb2af10 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts @@ -1,22 +1,17 @@ const { rest } = window.MockServiceWorker; -import type { OEmbedResult} from '@umbraco-cms/backoffice/modal'; -import { OEmbedStatus } from '@umbraco-cms/backoffice/modal'; +import type { OEmbedResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ - rest.get(umbracoPath('/rteembed'), (req, res, ctx) => { - const widthParam = req.url.searchParams.get('width'); + rest.get(umbracoPath('/oembed/query'), (req, res, ctx) => { + const widthParam = req.url.searchParams.get('maxWidth'); const width = widthParam ? parseInt(widthParam) : 360; - const heightParam = req.url.searchParams.get('height'); + const heightParam = req.url.searchParams.get('maxHeight'); const height = heightParam ? parseInt(heightParam) : 240; - const response: OEmbedResult = { - supportsDimensions: true, + const response: OEmbedResponseModel = { markup: ``, - oEmbedStatus: OEmbedStatus.Success, - width, - height, }; return res(ctx.status(200), ctx.json(response)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index 3d1091854d..4b02ecb73e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -171,9 +171,7 @@ export class UmbAuthFlow { const tokenResponseJson = await this.#storageBackend.getItem(UMB_STORAGE_TOKEN_RESPONSE_NAME); if (tokenResponseJson) { const response = new TokenResponse(JSON.parse(tokenResponseJson)); - if (response.isValid()) { - this.#tokenResponse = response; - } + this.#tokenResponse = response; } } @@ -225,13 +223,13 @@ export class UmbAuthFlow { } /** - * This method will check if the user is logged in by validating the timestamp of the stored token. + * This method will check if the user is logged in by validating if there is a token stored. * If no token is stored, it will return false. * * @returns true if the user is logged in, false otherwise. */ isAuthorized(): boolean { - return !!this.#tokenResponse && this.#tokenResponse.isValid(); + return !!this.#tokenResponse; } /** @@ -373,8 +371,10 @@ export class UmbAuthFlow { async #performTokenRequest(request: TokenRequest): Promise { try { this.#tokenResponse = await this.#tokenHandler.performTokenRequest(this.#configuration, request); + this.#saveTokenState(); } catch (error) { - // If the token request fails, it means the refresh token is invalid + // If the token request fails, it means the code or refresh token is invalid + this.clearTokenStorage(); console.error('Token request error', error); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-action-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-action-bundle.element.ts index 618bd45126..048cc93bae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-action-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-action-bundle.element.ts @@ -1,29 +1,10 @@ -import type { UmbDefaultCollectionContext } from '../default/collection-default.context.js'; -import { UMB_COLLECTION_CONTEXT } from '../default/collection-default.context.js'; -import { html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-collection-action-bundle') export class UmbCollectionActionBundleElement extends UmbLitElement { - #collectionContext?: UmbDefaultCollectionContext; - - @state() - _collectionAlias? = ''; - - constructor() { - super(); - - this.consumeContext(UMB_COLLECTION_CONTEXT, (context) => { - this.#collectionContext = context; - if (!this.#collectionContext) return; - this._collectionAlias = this.#collectionContext.getManifest()?.alias; - }); - } - render() { - return html` - ${this._collectionAlias ? html`` : nothing} - `; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts index 6a60bd56aa..f57b0043b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts @@ -32,15 +32,11 @@ export class UmbCollectionDefaultElement extends UmbLitElement { super(); this.consumeContext(UMB_COLLECTION_CONTEXT, (context) => { this.#collectionContext = context; + this.#collectionContext?.requestCollection(); this.#observeCollectionRoutes(); }); } - protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { - super.firstUpdated(_changedProperties); - this.#collectionContext?.requestCollection(); - } - #observeCollectionRoutes() { if (!this.#collectionContext) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts index 22be2b754a..d9d1ac6ca3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts @@ -75,6 +75,10 @@ export class UmbBodyLayoutElement extends LitElement { this.toggleAttribute('scrolling', this._scrollContainer.scrollTop > 0); }; + #setSlotVisibility(target: HTMLElement, hasChildren: boolean) { + target.style.display = hasChildren ? 'flex' : 'none'; + } + render() { return html`
{ this._headerSlotHasChildren = this.#hasNodes(e); + this.#setSlotVisibility(e.target as HTMLElement, this._headerSlotHasChildren); }}> { - this._actionsMenuSlotHasChildren = this.#hasNodes(e); + this._navigationSlotHasChildren = this.#hasNodes(e); + this.#setSlotVisibility(e.target as HTMLElement, this._navigationSlotHasChildren); }}> { this._actionsMenuSlotHasChildren = this.#hasNodes(e); + this.#setSlotVisibility(e.target as HTMLElement, this._actionsMenuSlotHasChildren); }}>
@@ -185,18 +192,16 @@ export class UmbBodyLayoutElement extends LitElement { } #header-slot, - #tabs-slot, #action-menu-slot, #navigation-slot { - display: flex; + display: none; height: 100%; align-items: center; box-sizing: border-box; min-width: 0; } - #navigation-slot, - #tabs-slot { + #navigation-slot { margin-left: auto; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts index 4a8fc02a3e..fb6044cc48 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts @@ -30,4 +30,5 @@ export * from './multiple-text-string-input/index.js'; export * from './popover-layout/index.js'; export * from './ref-item/index.js'; export * from './stack/index.js'; +export * from './split-panel/index.js'; export * from './table/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts index 0da0ec53c4..1c78030a0f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.element.ts @@ -1,9 +1,18 @@ -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +/** + * This element passes a datetime string to a regular HTML input element. + * + * @remark Be aware that you cannot include a time demonination, i.e. "10:44:00" if you + * set the input type of this element to "date". If you do, the browser will not show + * the value at all. + * + * @element umb-input-date + */ @customElement('umb-input-date') export class UmbInputDateElement extends UUIFormControlMixin(UmbLitElement, '') { protected getFormElement() { @@ -19,9 +28,6 @@ export class UmbInputDateElement extends UUIFormControlMixin(UmbLitElement, '') @property() type: 'date' | 'time' | 'datetime-local' = 'date'; - @property({ type: String }) - displayValue?: string; - @property({ type: String }) min?: string; @@ -31,59 +37,20 @@ export class UmbInputDateElement extends UUIFormControlMixin(UmbLitElement, '') @property({ type: Number }) step?: number; - connectedCallback(): void { - super.connectedCallback(); - - if (!this.value) return; - this.displayValue = this.#UTCToLocal(this.value as string); - } - - #localToUTC(date: string) { - if (this.type === 'time') { - return new Date(`${new Date().toJSON().slice(0, 10)} ${date}`).toISOString().slice(11, 16); - } else { - return new Date(date).toJSON(); - } - } - - #UTCToLocal(d: string) { - if (this.type === 'time') { - const local = new Date(`${new Date().toJSON().slice(0, 10)} ${d}Z`) - .toLocaleTimeString(undefined, { hourCycle: 'h23' }) - .slice(0, 5); - return local; - } else { - const timezoneReset = `${d.replace('Z', '')}Z`; - const date = new Date(timezoneReset); - - const dateString = `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${( - '0' + date.getDate() - ).slice(-2)}T${('0' + date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}:${( - '0' + date.getSeconds() - ).slice(-2)}`; - - return this.type === 'datetime-local' ? dateString : `${dateString.substring(0, 10)}`; - } - } - #onChange(event: UUIInputEvent) { - const newValue = event.target.value as string; - if (!newValue) return; - - this.value = this.#localToUTC(newValue); - this.displayValue = newValue; + this.value = event.target.value; this.dispatchEvent(new UmbChangeEvent()); } render() { return html` `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/index.ts new file mode 100644 index 0000000000..5e03122c68 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/index.ts @@ -0,0 +1,3 @@ +import './split-panel.element.js'; + +export * from './split-panel.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/split-panel.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/split-panel.element.ts new file mode 100644 index 0000000000..f7688b0081 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/split-panel.element.ts @@ -0,0 +1,297 @@ +import { + type PropertyValueMap, + LitElement, + css, + customElement, + html, + property, + query, + state, +} from '@umbraco-cms/backoffice/external/lit'; + +/** + * Custom element for a split panel with adjustable divider. + * @element umb-split-panel + * @slot start - Content for the start panel. + * @slot end - Content for the end panel. + * @cssprop --umb-split-panel-initial-position - Initial position of the divider. + * @cssprop --umb-split-panel-start-min-width - Minimum width of the start panel. + * @cssprop --umb-split-panel-end-min-width - Minimum width of the end panel. + * @cssprop --umb-split-panel-divider-touch-area-width - Width of the divider touch area. + * @cssprop --umb-split-panel-divider-width - Width of the divider. + * @cssprop --umb-split-panel-divider-color - Color of the divider. + */ +@customElement('umb-split-panel') +export class UmbSplitPanelElement extends LitElement { + @query('#main') mainElement!: HTMLElement; + @query('#divider-touch-area') dividerTouchAreaElement!: HTMLElement; + @query('#divider') dividerElement!: HTMLElement; + + /** + * Snap points for the divider position. + * Pixel or percent space-separated values: e.g., "100px 50% -75% -200px". + * Negative values are relative to the end of the container. + */ + @property({ type: String }) snap?: string; //TODO: Consider using css variables for snap points. + + /** + * Locking mode for the split panel. + * Possible values: "start", "end", "none" (default). + */ + @property({ type: String }) lock: 'start' | 'end' | 'none' = 'none'; + + /** + * Initial position of the divider. + * Pixel or percent value: e.g., "100px" or "25%". + * Defaults to a CSS variable if not set: "var(--umb-split-panel-initial-position) which defaults to 50%". + */ + @property({ type: String, reflect: true }) position = 'var(--umb-split-panel-initial-position)'; + //TODO: Add support for negative values (relative to end of container) similar to snap points. + + /** Width of the locked panel when in "start" or "end" lock mode */ + #lockedPanelWidth: number = 0; + /** Pixel value for the snap threshold. Determines how close the divider needs to be to a snap point to snap to it. */ + readonly #SNAP_THRESHOLD = 25 as const; + + @state() _hasStartPanel = false; + @state() _hasEndPanel = false; + get #hasBothPanels() { + return this._hasStartPanel && this._hasEndPanel; + } + + #hasInitialized = false; + + disconnectedCallback() { + super.disconnectedCallback(); + this.#disconnect(); + } + + protected updated(_changedProperties: PropertyValueMap | Map): void { + super.updated(_changedProperties); + + if (!this.#hasInitialized) return; + + if (_changedProperties.has('position')) { + if (this.lock !== 'none') { + const { width } = this.mainElement.getBoundingClientRect(); + + let pos = parseFloat(this.position); + + if (this.position.endsWith('%')) { + pos = (pos / 100) * width; + } + + const lockedPanelWidth = this.lock === 'start' ? pos : width - pos; + this.#lockedPanelWidth = lockedPanelWidth; + } + + this.#updateSplit(); + } + } + + #clamp(value: number, min: number, max: number) { + return Math.min(Math.max(value, min), max); + } + + #setPosition(pos: number) { + const { width } = this.mainElement.getBoundingClientRect(); + const localPos = this.#clamp(pos, 0, width); + const percentagePos = (localPos / width) * 100; + this.position = percentagePos + '%'; + } + + #updateSplit() { + // If lock is none + let maxStartWidth = this.position; + let maxEndWidth = '1fr'; + + if (this.lock === 'start') { + maxStartWidth = this.#lockedPanelWidth + 'px'; + maxEndWidth = `1fr`; + } + if (this.lock === 'end') { + maxStartWidth = `1fr`; + maxEndWidth = this.#lockedPanelWidth + 'px'; + } + + this.mainElement.style.gridTemplateColumns = ` + minmax(var(--umb-split-panel-start-min-width), ${maxStartWidth}) + 0px + minmax(var(--umb-split-panel-end-min-width), ${maxEndWidth}) + `; + } + + #onDragStart = (event: PointerEvent | TouchEvent) => { + event.preventDefault(); + + const move = (event: PointerEvent) => { + const { clientX } = event; + const { left, width } = this.mainElement.getBoundingClientRect(); + const localPos = this.#clamp(clientX - left, 0, width); + const mappedPos = mapXAxisToSnap(localPos, width); + + this.#lockedPanelWidth = this.lock === 'start' ? mappedPos : width - mappedPos; + this.#setPosition(mappedPos); + }; + + const stop = () => { + document.removeEventListener('pointermove', move); + document.removeEventListener('pointerup', stop); + + this.dispatchEvent(new CustomEvent('position-changed', { detail: { position: this.position } })); + }; + + const mapXAxisToSnap = (xPos: number, containerWidth: number) => { + const snaps = this.snap?.split(' '); + if (!snaps) return xPos; + + const snapsInPixels = snaps.map((snap) => { + let snapPx = parseFloat(snap); + + if (snap.endsWith('%')) { + snapPx = (snapPx / 100) * containerWidth; + } + + if (snap.startsWith('-')) { + snapPx = containerWidth + snapPx; + } + + return snapPx; + }); + + const closestSnap = snapsInPixels.reduce((prev, curr) => { + return Math.abs(curr - xPos) < Math.abs(prev - xPos) ? curr : prev; + }); + + if (closestSnap < xPos + this.#SNAP_THRESHOLD && closestSnap > xPos - this.#SNAP_THRESHOLD) { + xPos = closestSnap; + } + + return xPos; + }; + + document.addEventListener('pointermove', move, { passive: true }); + document.addEventListener('pointerup', stop); + }; + + #disconnect() { + this.dividerTouchAreaElement.removeEventListener('pointerdown', this.#onDragStart); + this.dividerTouchAreaElement.removeEventListener('touchstart', this.#onDragStart); + this.dividerElement.style.display = 'none'; + this.mainElement.style.display = 'flex'; + this.#hasInitialized = false; + } + + async #connect() { + this.#hasInitialized = true; + + this.mainElement.style.display = 'grid'; + this.mainElement.style.gridTemplateColumns = `${this.position} 0px 1fr`; + this.dividerElement.style.display = 'unset'; + + this.dividerTouchAreaElement.addEventListener('pointerdown', this.#onDragStart); + this.dividerTouchAreaElement.addEventListener('touchstart', this.#onDragStart, { passive: false }); + + // Wait for the next frame to get the correct position of the divider. + await new Promise((resolve) => requestAnimationFrame(resolve)); + + const { left: dividerLeft } = this.shadowRoot!.querySelector('#divider')!.getBoundingClientRect(); + const { left: mainLeft, width: mainWidth } = this.mainElement.getBoundingClientRect(); + const percentagePos = ((dividerLeft - mainLeft) / mainWidth) * 100; + this.position = `${percentagePos}%`; + } + + #onSlotChanged(event: Event) { + const slot = event.target as HTMLSlotElement; + const name = slot.name; + + if (name === 'start') { + this._hasStartPanel = slot.assignedElements().length > 0; + } + if (name === 'end') { + this._hasEndPanel = slot.assignedElements().length > 0; + } + + if (!this.#hasBothPanels) { + if (this.#hasInitialized) { + this.#disconnect(); + } + return; + } + + this.#connect(); + } + + render() { + return html` +
+ +
+
+
+ +
+ `; + } + static styles = css` + :host { + display: contents; + --umb-split-panel-initial-position: 50%; + --umb-split-panel-start-min-width: 0; + --umb-split-panel-end-min-width: 0; + --umb-split-panel-divider-touch-area-width: 20px; + --umb-split-panel-divider-width: 1px; + --umb-split-panel-divider-color: transparent; + --umb-split-panel-slot-overflow: hidden; + } + slot { + overflow: var(--umb-split-panel-slot-overflow); + display: block; + min-height: 0; + } + #main { + width: 100%; + height: 100%; + display: flex; + position: relative; + z-index: 0; + overflow: hidden; + } + #divider { + height: 100%; + position: relative; + z-index: 999999; + display: none; + } + #divider-touch-area { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: var(--umb-split-panel-divider-touch-area-width); + transform: translateX(-50%); + cursor: col-resize; + } + /* Do we want a line that shows the divider? */ + #divider::after { + content: ''; + position: absolute; + top: 0; + left: 50%; + width: var(--umb-split-panel-divider-width); + height: 100%; + transform: round(translateX(-50%)); + background-color: var(--umb-split-panel-divider-color); + z-index: -1; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-split-panel': UmbSplitPanelElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/split-panel.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/split-panel.stories.ts new file mode 100644 index 0000000000..3cb9752a4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/split-panel/split-panel.stories.ts @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/web-components'; +import './split-panel.element.js'; +import type { UmbSplitPanelElement } from './split-panel.element.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +const meta: Meta = { + title: 'Components/Split Panel', + component: 'umb-split-panel', + argTypes: { + lock: { options: ['none', 'start', 'end'] }, + snap: { control: 'text' }, + position: { control: 'text' }, + }, + args: { + lock: 'start', + snap: '', + position: '50%', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = { + render: (props) => html` + +
Start
+
End
+
+ + `, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts index bd97af52bb..f42f8a68f8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -31,7 +31,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements identifier: 'content-type-tabs-sorter', itemSelector: 'uui-tab', containerSelector: 'uui-tab-group', - disabledItemSelector: '#root-tab', + disabledItemSelector: ':not([sortable])', resolvePlacement: (args) => args.relatedRect.left + args.relatedRect.width * 0.5 > args.pointerX, onChange: ({ model }) => { this._tabs = model; @@ -47,30 +47,30 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements // Doesn't exist in model if (newIndex === -1) return; - // First in list - if (newIndex === 0 && model.length > 1) { - this.#tabsStructureHelper.partialUpdateContainer(item.id, { sortOrder: model[1].sortOrder - 1 }); - return; + // As origin we set prev sort order to -1, so if no other then our item will become 0 + let prevSortOrder = -1; + + // If not first in list, then get the sortOrder of the item before. [NL] + if (newIndex > 0 && model.length > 0) { + prevSortOrder = model[newIndex - 1].sortOrder; } - // Not first in list - if (newIndex > 0 && model.length > 1) { - const prevItemSortOrder = model[newIndex - 1].sortOrder; + // increase the prevSortOrder and use it for the moved item, + this.#tabsStructureHelper.partialUpdateContainer(item.id, { + sortOrder: ++prevSortOrder, + }); - let weight = 1; - this.#tabsStructureHelper.partialUpdateContainer(item.id, { sortOrder: prevItemSortOrder + weight }); - - // Check for overlaps - // TODO: Make sure this take inheritance into considerations. - model.some((entry, index) => { - if (index <= newIndex) return; - if (entry.sortOrder === prevItemSortOrder + weight) { - weight++; - this.#tabsStructureHelper.partialUpdateContainer(entry.id, { sortOrder: prevItemSortOrder + weight }); - } - // Break the loop - return true; + // Adjust everyone right after, until there is a gap between the sortOrders: [NL] + let i = newIndex + 1; + let entry: UmbPropertyTypeContainerModel | undefined; + // As long as there is an item with the index & the sortOrder is less or equal to the prevSortOrder, we will update the sortOrder: + while ((entry = model[i]) !== undefined && entry.sortOrder <= prevSortOrder) { + // Increase the prevSortOrder and use it for the item: + this.#tabsStructureHelper.partialUpdateContainer(entry.id, { + sortOrder: ++prevSortOrder, }); + + i++; } }, }); @@ -399,7 +399,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements ? this.localize.term('general_reorderDone') : this.localize.term('general_reorder'); - return html`
+ return html`
${this._compositionRepositoryAlias ? html` + data-umb-tab-id=${ifDefined(tab.id)} + ?sortable=${ownedTab}> ${this.renderTabInner(tab, tabActive, ownedTab)} `; } @@ -581,6 +582,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements position: relative; border-left: 1px hidden transparent; border-right: 1px solid var(--uui-color-border); + background-color: var(--uui-color-surface); } .not-active uui-button { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/culture/components/input-culture-select/input-culture-select.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/culture/components/input-culture-select/input-culture-select.element.ts index b8bf9fdc87..ea5dfb3295 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/culture/components/input-culture-select/input-culture-select.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/culture/components/input-culture-select/input-culture-select.element.ts @@ -71,7 +71,7 @@ export class UmbInputCultureSelectElement extends UUIFormControlMixin(UmbLitElem } get #fromAvailableCultures() { - return this._cultures.find((culture) => culture.name.toLowerCase() === (this.value as string).toLowerCase()); + return this._cultures.find((culture) => culture.name.toLowerCase() === (this.value as string)?.toLowerCase()); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts index 34461d7f3d..dc163ed815 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts @@ -14,11 +14,21 @@ export class UmbSectionAliasCondition { constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { super(host, args); - this.consumeContext(UMB_SECTION_CONTEXT, (context) => { - this.observe(context.alias, (sectionAlias) => { - this.permitted = sectionAlias === this.config.match; + + let permissionCheck: ((sectionAlias: string) => boolean) | undefined = undefined; + if (this.config.match) { + permissionCheck = (sectionAlias: string) => sectionAlias === this.config.match; + } else if (this.config.oneOf) { + permissionCheck = (sectionAlias: string) => this.config.oneOf!.indexOf(sectionAlias) !== -1; + } + + if (permissionCheck !== undefined) { + this.consumeContext(UMB_SECTION_CONTEXT, (context) => { + this.observe(context.alias, (sectionAlias) => { + this.permitted = sectionAlias ? permissionCheck!(sectionAlias) : false; + }); }); - }); + } } } @@ -29,6 +39,13 @@ export type SectionAliasConditionConfig = UmbConditionConfigBase<'Umb.Condition. * @example "Umb.Section.Content" */ match: string; + /** + * Define one or more workspaces that this extension should be available in + * + * @example + * ["Umb.Section.Content", "Umb.Section.Media"] + */ + oneOf?: Array; }; export const manifest: ManifestCondition = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts index 0e236841bf..96da62feb7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/confirm/confirm-modal.element.ts @@ -24,13 +24,17 @@ export class UmbConfirmModalElement extends UmbLitElement { ${this.data?.content} - + diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts index a0177a7f7a..43f43d1823 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts @@ -1,235 +1,146 @@ -import { css, html, unsafeHTML, when, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbOEmbedRepository } from './repository/oembed.repository.js'; +import { css, html, unsafeHTML, when, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { - OEmbedResult, - UmbEmbeddedMediaModalData, - UmbEmbeddedMediaModalValue} from '@umbraco-cms/backoffice/modal'; -import { - OEmbedStatus, - UmbModalBaseElement, -} from '@umbraco-cms/backoffice/modal'; -import { umbracoPath } from '@umbraco-cms/backoffice/utils'; - -interface UmbEmbeddedMediaModalModel { - url?: string; - info?: string; - a11yInfo?: string; - originalWidth: number; - originalHeight: number; - width: number; - height: number; - constrain: boolean; -} +import type { UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue } from '@umbraco-cms/backoffice/modal'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import type { UUIButtonState, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-embedded-media-modal') export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue > { - #loading = false; - #embedResult!: OEmbedResult; - - #handleConfirm() { - this.value = { - preview: this.#embedResult.markup, - originalWidth: this._model.width, - originalHeight: this._model.originalHeight, - width: this.#embedResult.width, - height: this.#embedResult.height, - }; - this.modalContext?.submit(); - } - - #handleCancel() { - this.modalContext?.reject(); - } + #oEmbedRepository = new UmbOEmbedRepository(this); + #validUrl?: string; @state() - private _model: UmbEmbeddedMediaModalModel = { - url: '', - width: 360, - height: 240, - constrain: true, - info: '', - a11yInfo: '', - originalHeight: 240, - originalWidth: 360, - }; + private _loading?: UUIButtonState; + + @state() + private _width = 360; + + @state() + private _height = 240; + + @state() + private _url = ''; connectedCallback() { super.connectedCallback(); + if (this.data?.width) this._width = this.data.width; + if (this.data?.height) this._height = this.data.height; + if (this.data?.constrain) this.value = { ...this.value, constrain: this.data.constrain }; + if (this.data?.url) { - Object.assign(this._model, this.data); + this._url = this.data.url; this.#getPreview(); } } async #getPreview() { - this._model.info = ''; - this._model.a11yInfo = ''; + this._loading = 'waiting'; - this.#loading = true; - this.requestUpdate('_model'); + const { data } = await this.#oEmbedRepository.requestOEmbed({ + url: this._url, + maxWidth: this._width, + maxHeight: this._height, + }); - try { - // TODO => use backend cli when available - const result = await fetch( - umbracoPath('/rteembed?') + - new URLSearchParams({ - url: this._model.url, - width: this._model.width?.toString(), - height: this._model.height?.toString(), - } as { [key: string]: string }), - ); - - this.#embedResult = await result.json(); - - switch (this.#embedResult.oEmbedStatus) { - case 0: - this.#onPreviewFailed('Not supported'); - break; - case 1: - this.#onPreviewFailed('Could not embed media - please ensure the URL is valid'); - break; - case 2: - this._model.info = ''; - this._model.a11yInfo = 'Retrieved URL'; - break; - } - } catch (e) { - this.#onPreviewFailed('Could not embed media - please ensure the URL is valid'); + if (data) { + this.#validUrl = this._url; + this.value = { ...this.value, markup: data.markup, url: this.#validUrl }; + this._loading = 'success'; + } else { + this.#validUrl = undefined; + this._loading = 'failed'; } - - this.#loading = false; - this.requestUpdate('_model'); } - #onPreviewFailed(message: string) { - this._model.info = message; - this._model.a11yInfo = message; + #onUrlChange(e: UUIInputEvent) { + this._url = e.target.value as string; } - #onUrlChange(e: InputEvent) { - this._model.url = (e.target as HTMLInputElement).value; - this.requestUpdate('_model'); + #onWidthChange(e: UUIInputEvent) { + this._width = parseInt(e.target.value as string, 10); + this.#getPreview(); } - #onWidthChange(e: InputEvent) { - this._model.width = parseInt((e.target as HTMLInputElement).value, 10); - this.#changeSize('width'); - } - - #onHeightChange(e: InputEvent) { - this._model.height = parseInt((e.target as HTMLInputElement).value, 10); - this.#changeSize('height'); - } - - /** - * Calculates the width or height axis dimension when the other is changed. - * If constrain is false, axis change independently - * @param axis {string} - */ - #changeSize(axis: 'width' | 'height') { - const resize = this._model.originalWidth !== this._model.width || this._model.originalHeight !== this._model.height; - - if (this._model.constrain) { - if (axis === 'width') { - this._model.height = Math.round((this._model.width / this._model.originalWidth) * this._model.height); - } else { - this._model.width = Math.round((this._model.height / this._model.originalHeight) * this._model.width); - } - } - - this._model.originalWidth = this._model.width; - this._model.originalHeight = this._model.height; - - if (this._model.url !== '' && resize) { - this.#getPreview(); - } + #onHeightChange(e: UUIInputEvent) { + this._height = parseInt(e.target.value as string, 10); + this.#getPreview(); } #onConstrainChange() { - this._model.constrain = !this._model.constrain; - } - - /** - * If the embed does not support dimensions, or was not requested successfully - * the width, height and constrain controls are disabled - * @returns {boolean} - */ - #dimensionControlsDisabled() { - return !this.#embedResult?.supportsDimensions || this.#embedResult?.oEmbedStatus !== OEmbedStatus.Success; + const constrain = !this.value?.constrain; + this.value = { ...this.value, constrain }; } render() { return html` - +
- + + label=${this.localize.term('general_retrieve')}>
${when( - this.#embedResult?.oEmbedStatus === OEmbedStatus.Success || this._model.a11yInfo, + this.#validUrl !== undefined, () => - html` + html`
- ${when(this.#loading, () => html``)} - ${when(this.#embedResult?.markup, () => html`${unsafeHTML(this.#embedResult.markup)}`)} - ${when(this._model.info, () => html` `)} - ${when( - this._model.a11yInfo, - () => html` `, - )} + ${when(this._loading === 'waiting', () => html``)} + ${when(this.value?.markup, () => html`${unsafeHTML(this.value.markup)}`)}
`, )} - + + @change=${this.#onWidthChange} + ?disabled=${this.#validUrl ? false : true}> - + + @change=${this.#onHeightChange} + ?disabled=${this.#validUrl ? false : true}> - + + .checked=${this.value?.constrain ?? false}>
- Cancel + this.modalContext?.reject()}> + label=${this.localize.term('buttons_confirmActionConfirm')} + @click=${() => this.modalContext?.submit()}>
`; } @@ -237,27 +148,11 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< static styles = [ UmbTextStyles, css` - h3 { - margin-left: var(--uui-size-space-5); - margin-right: var(--uui-size-space-5); - } - uui-input { width: 100%; --uui-button-border-radius: 0; } - .sr-only { - clip: rect(0, 0, 0, 0); - border: 0; - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - } - umb-property-layout:first-child { padding-top: 0; } @@ -265,10 +160,6 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< umb-property-layout:last-child { padding-bottom: 0; } - - p { - margin-bottom: 0; - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/index.ts new file mode 100644 index 0000000000..3d76f338dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/index.ts @@ -0,0 +1 @@ +export * from './repository/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/manifests.ts new file mode 100644 index 0000000000..9cf9968724 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/manifests.ts @@ -0,0 +1,13 @@ +import { manifests as repositories } from './repository/manifests.js'; +import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.EmbeddedMedia', + name: 'Embedded Media Modal', + element: () => import('./embedded-media-modal.element.js'), + }, +]; + +export const manifests = [...modals, ...repositories]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/index.ts new file mode 100644 index 0000000000..1a6e303708 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/index.ts @@ -0,0 +1,2 @@ +export { UmbOEmbedRepository } from './oembed.repository.js'; +export { UMB_OEMBED_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/manifests.ts new file mode 100644 index 0000000000..580f3e1026 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/manifests.ts @@ -0,0 +1,13 @@ +import { UmbOEmbedRepository } from './oembed.repository.js'; +import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_OEMBED_REPOSITORY_ALIAS = 'Umb.Repository.OEmbed'; + +const repository: ManifestRepository = { + type: 'repository', + alias: UMB_OEMBED_REPOSITORY_ALIAS, + name: 'OEmbed Repository', + api: UmbOEmbedRepository, +}; + +export const manifests: Array = [repository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.repository.ts new file mode 100644 index 0000000000..344c067453 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.repository.ts @@ -0,0 +1,20 @@ +import { UmbOEmbedServerDataSource } from './oembed.server.data.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; + +export class UmbOEmbedRepository extends UmbControllerBase implements UmbApi { + #dataSource = new UmbOEmbedServerDataSource(this); + + constructor(host: UmbControllerHost) { + super(host); + } + + async requestOEmbed({ url, maxWidth, maxHeight }: { url?: string; maxWidth?: number; maxHeight?: number }) { + const { data, error } = await this.#dataSource.getOEmbedQuery({ url, maxWidth, maxHeight }); + if (!error) { + return { data }; + } + return { error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.server.data.ts new file mode 100644 index 0000000000..88c45e3eb4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/repository/oembed.server.data.ts @@ -0,0 +1,31 @@ +import { OEmbedService } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * A data source for the OEmbed that fetches data from a given URL. + * @export + * @class UmbOEmbedServerDataSource + * @implements {RepositoryDetailDataSource} + */ +export class UmbOEmbedServerDataSource { + #host: UmbControllerHost; + + /** + * Creates an instance of UmbOEmbedServerDataSource. + * @param {UmbControllerHost} host + * @memberof UmbOEmbedServerDataSource + */ + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Fetches markup for the given URL. + * @param {string} unique + * @memberof UmbOEmbedServerDataSource + */ + async getOEmbedQuery({ url, maxWidth, maxHeight }: { url?: string; maxWidth?: number; maxHeight?: number }) { + return tryExecuteAndNotify(this.#host, OEmbedService.getOembedQuery({ url, maxWidth, maxHeight })); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts index 199908b6de..0b86ba4bf8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts @@ -1,4 +1,4 @@ -import type { UUIColorSwatchesEvent, UUIIconElement } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIColorSwatchesEvent } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, state, repeat, query, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -64,12 +64,9 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement + @click=${(e: InputEvent) => this.#changeIcon(e, icon.name)} + @keyup=${(e: KeyboardEvent) => this.#changeIcon(e, icon.name)}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/index.ts new file mode 100644 index 0000000000..5a1c0033a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/index.ts @@ -0,0 +1 @@ +export * from './embedded-media/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/confirm-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/confirm-modal.token.ts index f9feaa6d97..45bd9f9709 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/confirm-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/confirm-modal.token.ts @@ -5,6 +5,7 @@ export interface UmbConfirmModalData { headline: string; content: TemplateResult | string; color?: 'positive' | 'danger'; + cancelLabel?: string; confirmLabel?: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts index 7eca2cf44c..0fb7df92ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts @@ -1,31 +1,18 @@ import { UmbModalToken } from './modal-token.js'; -export enum OEmbedStatus { - NotSupported, - Error, - Success, -} - -export interface UmbEmbeddedMediaDimensions { - width: number; - height: number; - constrain?: boolean; -} - -export interface UmbEmbeddedMediaModalData extends UmbEmbeddedMediaDimensions { +export interface UmbEmbeddedMediaModalData extends Partial { url?: string; } -export interface OEmbedResult extends UmbEmbeddedMediaDimensions { - oEmbedStatus: OEmbedStatus; - supportsDimensions: boolean; - markup?: string; +export interface UmbEmbeddedMediaDimensionsModel { + constrain: boolean; + width: number; + height: number; } export interface UmbEmbeddedMediaModalValue extends UmbEmbeddedMediaModalData { - preview?: string; - originalWidth: number; - originalHeight: number; + markup: string; + url: string; } export const UMB_EMBEDDED_MEDIA_MODAL = new UmbModalToken( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts index d04b844492..a2958ea597 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts @@ -4,7 +4,6 @@ export * from './confirm-modal.token.js'; export * from './debug-modal.token.js'; export * from './embedded-media-modal.token.js'; export * from './entity-user-permission-settings-modal.token.js'; -export * from './examine-fields-settings-modal.token.js'; export * from './icon-picker-modal.token.js'; export * from './item-picker-modal.token.js'; export * from './modal-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-default.element.ts index b30d7ac991..bfb1103f32 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-default.element.ts @@ -44,6 +44,9 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio UmbExtensionElementInitializer >; + @state() + _splitPanelPosition = '300px'; + constructor() { super(); @@ -54,6 +57,11 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio }); this.#createRoutes(); + + const splitPanelPosition = localStorage.getItem('umb-split-panel-position'); + if (splitPanelPosition) { + this._splitPanelPosition = splitPanelPosition; + } } #createRoutes() { @@ -75,26 +83,37 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio ]; } + #onSplitPanelChange(event: CustomEvent) { + const position = event.detail.position; + localStorage.setItem('umb-split-panel-position', position.toString()); + } + render() { return html` - ${this._sidebarApps && this._sidebarApps.length > 0 - ? html` - - - ${repeat( - this._sidebarApps, - (app) => app.alias, - (app) => app.component, - )} - - ` - : nothing} - - ${this._routes && this._routes.length > 0 - ? html`` + + ${this._sidebarApps && this._sidebarApps.length > 0 + ? html` + + + ${repeat( + this._sidebarApps, + (app) => app.alias, + (app) => app.component, + )} + + ` : nothing} - - + + ${this._routes && this._routes.length > 0 + ? html`` + : nothing} + + + `; } @@ -106,6 +125,18 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio height: 100%; display: flex; } + + umb-split-panel { + --umb-split-panel-start-min-width: 200px; + --umb-split-panel-start-max-width: 400px; + --umb-split-panel-end-min-width: 600px; + --umb-split-panel-slot-overflow: visible; + } + @media only screen and (min-width: 800px) { + umb-split-panel { + --umb-split-panel-initial-position: 300px; + } + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar-context-menu/section-sidebar-context-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar-context-menu/section-sidebar-context-menu.element.ts index c1cdbe7762..6757c34d29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar-context-menu/section-sidebar-context-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar-context-menu/section-sidebar-context-menu.element.ts @@ -136,10 +136,10 @@ export class UmbSectionSidebarContextMenuElement extends UmbLitElement { } #action-modal { position: absolute; - left: var(--umb-section-sidebar-width); height: 100%; z-index: 1; top: 0; + right: calc(var(--umb-section-sidebar-width) * -1); width: var(--umb-section-sidebar-width); border: none; border-left: 1px solid var(--uui-color-border); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts index 65da98a094..5c86779064 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-sidebar/section-sidebar.element.ts @@ -29,6 +29,7 @@ export class UmbSectionSidebarElement extends UmbLitElement { display: flex; flex-direction: column; z-index: 10; + position: relative; } #scroll-container { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts index f62071f6c3..dcfcf0f2be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts @@ -14,11 +14,7 @@ const entityActions: Array = [ name: 'Create Document Type Entity Action', weight: 1200, api: UmbCreateDocumentTypeEntityAction, - forEntityTypes: [ - UMB_DOCUMENT_TYPE_ENTITY_TYPE, - UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, - UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE, - ], + forEntityTypes: [UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE], meta: { icon: 'icon-add', label: '#actions_create', diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts index a8c3c15d98..0809919909 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts @@ -115,7 +115,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement { #toggle { color: var(--uui-color-text); - width: var(--umb-section-sidebar-width); + width: 100%; text-align: left; background: none; border: none; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts index 8d4b2af962..2ba730a984 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts @@ -5,6 +5,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; // TODO: Make a store for the App Languages. // TODO: Implement default language end-point, in progress at backend team, so we can avoid getting all languages. @@ -28,7 +29,14 @@ export class UmbAppLanguageContext extends UmbContextBase constructor(host: UmbControllerHost) { super(host, UMB_APP_LANGUAGE_CONTEXT); this.#languageCollectionRepository = new UmbLanguageCollectionRepository(this); - this.#observeLanguages(); + + // TODO: We need to ensure this request is called every time the user logs in, but this should be done somewhere across the app and not here [JOV] + this.consumeContext(UMB_AUTH_CONTEXT, (authContext) => { + this.observe(authContext.isAuthorized, (isAuthorized) => { + if (!isAuthorized) return; + this.#observeLanguages(); + }); + }); } setLanguage(unique: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts index 664a71aefe..6099653631 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/repository/detail/language-detail.server.data-source.ts @@ -69,11 +69,11 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource
- + ${this._isNew + ? html` ` + : this._language?.name}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts index b75fadfff8..19c75cb63a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/repository/media-collection.server.data-source.ts @@ -32,12 +32,15 @@ export class UmbMediaCollectionServerDataSource implements UmbCollectionDataSour const model: UmbMediaCollectionItemModel = { unique: item.id, + entityType: 'media', + contentTypeAlias: item.mediaType.alias, createDate: new Date(variant.createDate), creator: item.creator, icon: item.mediaType.icon, name: variant.name, sortOrder: item.sortOrder, updateDate: new Date(variant.updateDate), + updater: item.creator, // TODO: Check if the `updater` is available for media items. [LK] values: item.values.map((item) => { return { alias: item.alias, value: item.value as string }; }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts index 1ae06b64f7..57d968794a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts @@ -10,11 +10,20 @@ export interface UmbMediaCollectionFilterModel extends UmbCollectionFilterModel export interface UmbMediaCollectionItemModel { unique: string; + entityType: string; + contentTypeAlias: string; createDate: Date; creator?: string | null; icon: string; name: string; sortOrder: number; updateDate: Date; + updater?: string | null; values: Array<{ alias: string; value: string }>; } + +export interface UmbEditableMediaCollectionItemModel { + item: UmbMediaCollectionItemModel; + editPath: string; +} + diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index 6a77f11469..43201b8839 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -1,12 +1,17 @@ -import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../types.js'; -import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; +import type { UmbMediaCollectionItemModel } from '../../types.js'; +import type { UmbMediaCollectionContext } from '../../media-collection.context.js'; +import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; -import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; @customElement('umb-media-grid-collection-view') export class UmbMediaGridCollectionViewElement extends UmbLitElement { + @state() + private _editMediaPath = ''; + @state() private _items: Array = []; @@ -16,7 +21,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { @state() private _selection: Array = []; - #collectionContext?: UmbDefaultCollectionContext; + #collectionContext?: UmbMediaCollectionContext; constructor() { super(); @@ -24,23 +29,43 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { this.#collectionContext = collectionContext; this.#observeCollectionContext(); }); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('media') + .onSetup(() => { + return { data: { entityType: 'media', preset: {} } }; + }) + .onReject(() => { + this.#collectionContext?.requestCollection(); + }) + .onSubmit(() => { + this.#collectionContext?.requestCollection(); + }) + .observeRouteBuilder((routeBuilder) => { + this._editMediaPath = routeBuilder({}); + }); } #observeCollectionContext() { if (!this.#collectionContext) return; - this.observe(this.#collectionContext.items, (items) => (this._items = items), 'umbCollectionItemsObserver'); + this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading'); + + this.observe(this.#collectionContext.items, (items) => (this._items = items), '_observeItems'); this.observe( this.#collectionContext.selection.selection, (selection) => (this._selection = selection), - 'umbCollectionSelectionObserver', + '_observeSelection', ); } - #onOpen(item: UmbMediaCollectionItemModel) { - //TODO: Fix when we have dynamic routing - history.pushState(null, '', 'section/media/workspace/media/edit/' + item.unique); + #onOpen(event: Event, unique: string) { + event.preventDefault(); + event.stopPropagation(); + + const url = this._editMediaPath + UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN.generateLocal({ unique }); + window.history.pushState(null, '', url); } #onSelect(item: UmbMediaCollectionItemModel) { @@ -60,26 +85,37 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { } render() { - if (this._loading) { - return html`
`; - } - - if (this._items.length === 0) { - return html`

${this.localize.term('content_listViewNoItems')}

`; - } + return this._items.length === 0 ? this.#renderEmpty() : this.#renderItems(); + } + #renderEmpty() { + if (this._items.length > 0) return nothing; return html` -
- ${repeat( - this._items, - (item) => item.unique, - (item) => this.#renderCard(item), +
+ ${when( + this._loading, + () => html``, + () => html`

${this.localize.term('content_listViewNoItems')}

`, )}
`; } - #renderCard(item: UmbMediaCollectionItemModel) { + #renderItems() { + if (this._items.length === 0) return nothing; + return html` +
+ ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderItem(item), + )} +
+ ${when(this._loading, () => html``)} + `; + } + + #renderItem(item: UmbMediaCollectionItemModel) { // TODO: Fix the file extension when media items have a file extension. [?] return html` 0} ?selected=${this.#isSelected(item)} - @open=${() => this.#onOpen(item)} + @open=${(event: Event) => this.#onOpen(event, item.unique)} @selected=${() => this.#onSelect(item)} @deselected=${() => this.#onDeselect(item)} class="media-item" diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts index b1a10fb899..688b2a90c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/column-layouts/media-table-column-name.element.ts @@ -1,50 +1,32 @@ -import type { UmbMediaCollectionItemModel } from '../../../types.js'; -import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbEditableMediaCollectionItemModel } from '../../../types.js'; +import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '@umbraco-cms/backoffice/components'; +import type { UUIButtonElement } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-media-table-column-name') export class UmbMediaTableColumnNameElement extends UmbLitElement implements UmbTableColumnLayoutElement { - @state() - private _editMediaPath = ''; - - @property({ type: Object, attribute: false }) column!: UmbTableColumn; - - @property({ type: Object, attribute: false }) item!: UmbTableItem; @property({ attribute: false }) - value!: UmbMediaCollectionItemModel; + value!: UmbEditableMediaCollectionItemModel; - constructor() { - super(); - - new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) - .addAdditionalPath('media') - .onSetup(() => { - return { data: { entityType: 'media', preset: {} } }; - }) - .observeRouteBuilder((routeBuilder) => { - this._editMediaPath = routeBuilder({}); - }); - } - - #onClick(event: Event) { - // TODO: [LK] Review the `stopPropagation` usage, as it causes a page reload. - // But we still need a say to prevent the `umb-table` from triggering a selection event. + #onClick(event: Event & { target: UUIButtonElement }) { + event.preventDefault(); event.stopPropagation(); + window.history.pushState(null, '', event.target.href); } render() { - return html``; + if (!this.value) return nothing; + return html` + + `; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts index a3a4cef9a6..86d2451247 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts @@ -1,6 +1,7 @@ +import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js'; import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../types.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; @@ -14,6 +15,8 @@ import type { UmbTableOrderedEvent, UmbTableSelectedEvent, } from '@umbraco-cms/backoffice/components'; +import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal'; import './column-layouts/media-table-column-name.element.js'; @@ -51,29 +54,52 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { @state() private _selection: Array = []; - @state() - private _skip: number = 0; - #collectionContext?: UmbDefaultCollectionContext; + #routeBuilder?: UmbModalRouteBuilder; + constructor() { super(); this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => { this.#collectionContext = collectionContext; - this.#observeCollectionContext(); }); + + this.#registerModalRoute(); + } + + #registerModalRoute() { + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath(':entityType') + .onSetup((params) => { + return { data: { entityType: params.entityType, preset: {} } }; + }) + .onReject(() => { + this.#collectionContext?.requestCollection(); + }) + .onSubmit(() => { + this.#collectionContext?.requestCollection(); + }) + .observeRouteBuilder((routeBuilder) => { + this.#routeBuilder = routeBuilder; + + // NOTE: Configuring the observations AFTER the route builder is ready, + // otherwise there is a race condition and `#collectionContext.items` tends to win. [LK] + this.#observeCollectionContext(); + }); } #observeCollectionContext() { if (!this.#collectionContext) return; + this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading'); + this.observe( this.#collectionContext.userDefinedProperties, (userDefinedProperties) => { this._userDefinedProperties = userDefinedProperties; this.#createTableHeadings(); }, - 'umbCollectionUserDefinedPropertiesObserver', + '_observeUserDefinedProperties', ); this.observe( @@ -82,7 +108,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { this._items = items; this.#createTableItems(this._items); }, - 'umbCollectionItemsObserver', + '_observeItems', ); this.observe( @@ -90,15 +116,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { (selection) => { this._selection = selection as string[]; }, - 'umbCollectionSelectionObserver', - ); - - this.observe( - this.#collectionContext.pagination.skip, - (skip) => { - this._skip = skip; - }, - 'umbCollectionSkipObserver', + '_observeSelection', ); } @@ -129,15 +147,21 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { const data = this._tableColumns?.map((column) => { + const editPath = this.#routeBuilder + ? this.#routeBuilder({ entityType: item.entityType }) + + UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN.generateLocal({ unique: item.unique }) + : ''; + return { columnAlias: column.alias, - value: column.elementName ? item : this.#getPropertyValueByAlias(item, column.alias), + value: column.elementName ? { item, editPath } : this.#getPropertyValueByAlias(item, column.alias), }; }) ?? []; return { id: item.unique, icon: item.icon, + entityType: 'media', data: data, }; }); @@ -145,6 +169,8 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { #getPropertyValueByAlias(item: UmbMediaCollectionItemModel, alias: string) { switch (alias) { + case 'contentTypeAlias': + return item.contentTypeAlias; case 'createDate': return item.createDate.toLocaleString(); case 'name': @@ -156,6 +182,8 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { return item.sortOrder; case 'updateDate': return item.updateDate.toLocaleString(); + case 'updater': + return item.updater; default: return item.values.find((value) => value.alias === alias)?.value ?? ''; } @@ -186,23 +214,34 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { } render() { - if (this._loading) { - return html`
`; - } + return this._tableItems.length === 0 ? this.#renderEmpty() : this.#renderItems(); + } - if (this._tableItems.length === 0) { - return html`

${this.localize.term('content_listViewNoItems')}

`; - } + #renderEmpty() { + if (this._tableItems.length > 0) return nothing; + return html` +
+ ${when( + this._loading, + () => html``, + () => html`

${this.localize.term('content_listViewNoItems')}

`, + )} +
+ `; + } + #renderItems() { + if (this._tableItems.length === 0) return nothing; return html` + @selected=${this.#handleSelect} + @deselected=${this.#handleDeselect} + @ordered=${this.#handleOrdering}> + ${when(this._loading, () => html``)} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index 51a8792071..84b18a3c44 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -6,6 +6,7 @@ export * from './workspace/index.js'; export * from './reference/index.js'; export * from './components/index.js'; export * from './entity.js'; +export * from './paths.js'; export * from './utils/index.js'; export { UMB_MEDIA_TREE_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL } from './tree/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts new file mode 100644 index 0000000000..d5eb6fdfd5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts @@ -0,0 +1,3 @@ +import { UmbPathPattern } from '@umbraco-cms/backoffice/router'; + +export const UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ unique: string }>('edit/:unique'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts index 48025bc6a3..096bd46d96 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts @@ -16,17 +16,19 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple @state() private _memberTypeIcon = ''; - private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; - private _memberTypeItemRepository: UmbMemberTypeItemRepository = new UmbMemberTypeItemRepository(this); - @state() private _editMemberTypePath = ''; - @state() private _createDate = 'Unknown'; @state() private _updateDate = 'Unknown'; + @state() + private _unique = ''; + + #workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; + #memberTypeItemRepository: UmbMemberTypeItemRepository = new UmbMemberTypeItemRepository(this); + constructor() { super(); @@ -40,12 +42,13 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple }); this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, async (context) => { - this._workspaceContext = context; - this.observe(this._workspaceContext.contentTypeUnique, (unique) => (this._memberTypeUnique = unique || '')); - this.observe(this._workspaceContext.createDate, (date) => (this._createDate = date || 'Unknown')); - this.observe(this._workspaceContext.updateDate, (date) => (this._updateDate = date || 'Unknown')); + this.#workspaceContext = context; + this.observe(this.#workspaceContext.contentTypeUnique, (unique) => (this._memberTypeUnique = unique || '')); + this.observe(this.#workspaceContext.createDate, (date) => (this._createDate = date || 'Unknown')); + this.observe(this.#workspaceContext.updateDate, (date) => (this._updateDate = date || 'Unknown')); + this.observe(this.#workspaceContext.unique, (unique) => (this._unique = unique || '')); - const memberType = (await this._memberTypeItemRepository.requestItems([this._memberTypeUnique])).data?.[0]; + const memberType = (await this.#memberTypeItemRepository.requestItems([this._memberTypeUnique])).data?.[0]; if (!memberType) return; this._memberTypeName = memberType.name; this._memberTypeIcon = memberType.icon; @@ -83,7 +86,7 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple
- ${this._memberTypeUnique} + ${this._unique}
`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts index 39ccfe8ba2..977830cf4c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/config/order-by/order-by.element.ts @@ -1,34 +1,44 @@ -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui'; -import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; -import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbCollectionColumnConfiguration } from '../../../../core/collection/types.js'; +import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; +import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui'; /** * @element umb-property-editor-ui-collection-order-by */ @customElement('umb-property-editor-ui-collection-order-by') export class UmbPropertyEditorUICollectionOrderByElement extends UmbLitElement implements UmbPropertyEditorUiElement { - private _value = ''; @property() - public set value(v: string) { - this._value = v; - this._options = this._options.map((option) => (option.value === v ? { ...option, selected: true } : option)); - } - public get value() { - return this._value; - } + public value: string = ''; + + public config?: UmbPropertyEditorConfigCollection; @state() - _options: Array