Merge branch 'chore/collect-multi-url-picker-files' of https://github.com/umbraco/Umbraco.CMS.Backoffice into chore/collect-multi-url-picker-files

This commit is contained in:
Mads Rasmussen
2024-05-10 18:26:30 +02:00
141 changed files with 2990 additions and 992 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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<UmbBackofficeContext> {
#activeSectionAlias = new UmbStringState(undefined);
@@ -26,7 +27,13 @@ export class UmbBackofficeContext extends UmbContextBase<UmbBackofficeContext> {
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() {

View File

@@ -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() {

View File

@@ -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',

View File

@@ -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',

View File

@@ -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.<br />\n Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.<br />\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.<br /> Afhængigt af hvor meget indhold der er på dit website, kan det tage et stykke tid.<br /> 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',

View File

@@ -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',

View File

@@ -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.<br />\n Depending on how much content there is in your site this could take a while.<br />\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.<br /> Depending on how much content there is in your site this could take a while.<br /> 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',

File diff suppressed because one or more lines are too long

View File

@@ -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',

View File

@@ -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',

View File

@@ -166,8 +166,8 @@ export default {
confirmlogout: 'האם הינך בטוח?',
confirmSure: 'האם אתה בטוח?',
cut: 'גזור',
editdictionary: 'ערוך פרט מילון',
editlanguage: 'ערוך שפה',
editDictionary: 'ערוך פרט מילון',
editLanguage: 'ערוך שפה',
insertAnchor: 'הוסף קישור מקומי',
insertCharacter: 'הוסף תו',
insertgraphicheadline: 'הוסף פס גרפי',

View File

@@ -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',

View File

@@ -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',

View File

@@ -215,8 +215,8 @@ export default {
confirmlogout: 'ログアウトしますか?',
confirmSure: '本当にいいですか?',
cut: '切り取り',
editdictionary: 'ディクショナリのアイテムの編集',
editlanguage: '言語の編集',
editDictionary: 'ディクショナリのアイテムの編集',
editLanguage: '言語の編集',
insertAnchor: 'アンカーの挿入',
insertCharacter: '文字の挿入',
insertgraphicheadline: 'ヘッドライン画像の挿入',

View File

@@ -166,8 +166,8 @@ export default {
confirmlogout: '로그아웃 하시겠습니까?',
confirmSure: '확실합니까?',
cut: "TRANSLATE ME: 'Cut'",
editdictionary: '사전 항목 편집',
editlanguage: '언어 편집',
editDictionary: '사전 항목 편집',
editLanguage: '언어 편집',
insertAnchor: '내부 링크삽입',
insertCharacter: '문자열 삽입',
insertgraphicheadline: '그래픽 헤드라인 삽입',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -347,8 +347,8 @@ export default {
confirmlogout: 'Вы уверены?',
confirmSure: 'Вы уверены?',
cut: 'Вырезать',
editdictionary: 'Править статью словаря',
editlanguage: 'Изменить язык',
editDictionary: 'Править статью словаря',
editLanguage: 'Изменить язык',
insertAnchor: 'Вставить локальную ссылку (якорь)',
insertCharacter: 'Вставить символ',
insertgraphicheadline: 'Вставить графический заголовок',

View File

@@ -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',

View File

@@ -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',

View File

@@ -346,8 +346,8 @@ export default {
confirmlogout: 'Ви впевнені?',
confirmSure: 'Ви впевнені?',
cut: 'Вирізати',
editdictionary: 'Редагувати статтю словника',
editlanguage: 'Змінити мову',
editDictionary: 'Редагувати статтю словника',
editLanguage: 'Змінити мову',
insertAnchor: 'Вставити локальне посилання (якір)',
insertCharacter: 'Вставити символ',
insertgraphicheadline: 'Вставити графічний заголовок',

View File

@@ -221,8 +221,8 @@ export default {
confirmlogout: '您确定吗?',
confirmSure: '您确定吗?',
cut: '剪切',
editdictionary: '编辑字典项',
editlanguage: '编辑语言',
editDictionary: '编辑字典项',
editLanguage: '编辑语言',
insertAnchor: '插入本地链接',
insertCharacter: '插入字符',
insertgraphicheadline: '插入图片标题',

View File

@@ -220,8 +220,8 @@ export default {
confirmlogout: '您確定嗎?',
confirmSure: '您確定嗎?',
cut: '剪切',
editdictionary: '編輯字典項',
editlanguage: '編輯語言',
editDictionary: '編輯字典項',
editLanguage: '編輯語言',
insertAnchor: '插入本地連結',
insertCharacter: '插入字元',
insertgraphicheadline: '插入圖片標題',

View File

@@ -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
}

View File

@@ -9022,20 +9022,17 @@ take
* @returns string Success
* @throws ApiError
*/
public static putWebhookById(data: WebhookData['payloads']['PutWebhookById']): CancelablePromise<WebhookData['responses']['PutWebhookById']> {
public static deleteWebhookById(data: WebhookData['payloads']['DeleteWebhookById']): CancelablePromise<WebhookData['responses']['DeleteWebhookById']> {
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<WebhookData['responses']['DeleteWebhookById']> {
public static putWebhookById(data: WebhookData['payloads']['PutWebhookById']): CancelablePromise<WebhookData['responses']['PutWebhookById']> {
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`,

View File

@@ -236,7 +236,7 @@ export const data: Array<UmbMockDataTypeModel> = [
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<UmbMockDataTypeModel> = [
id: 'dt-integer',
parent: null,
editorAlias: 'Umbraco.Integer',
editorUiAlias: 'Umb.PropertyEditorUi.Integer',
editorUiAlias: 'Umb.PropertyEditorUi.Number',
hasChildren: false,
isFolder: false,
isDeletable: true,

View File

@@ -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: '',

View File

@@ -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: `<iframe width="${width}" height="${height}" src="https://www.youtube.com/embed/wJNbtYdr-Hg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Sleep Token - The Summoning"></iframe>`,
oEmbedStatus: OEmbedStatus.Success,
width,
height,
};
return res(ctx.status(200), ctx.json(response));

View File

@@ -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<void> {
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);
}
}

View File

@@ -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<any, any>;
@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`<umb-extension-slot type="collectionAction"></umb-extension-slot>` : nothing}
`;
return html`<umb-extension-slot type="collectionAction"></umb-extension-slot>`;
}
}

View File

@@ -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<any> | Map<PropertyKey, unknown>): void {
super.firstUpdated(_changedProperties);
this.#collectionContext?.requestCollection();
}
#observeCollectionRoutes() {
if (!this.#collectionContext) return;

View File

@@ -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`
<div
@@ -92,18 +96,21 @@ export class UmbBodyLayoutElement extends LitElement {
name="header"
@slotchange=${(e: Event) => {
this._headerSlotHasChildren = this.#hasNodes(e);
this.#setSlotVisibility(e.target as HTMLElement, this._headerSlotHasChildren);
}}></slot>
<slot
id="navigation-slot"
name="navigation"
@slotchange=${(e: Event) => {
this._actionsMenuSlotHasChildren = this.#hasNodes(e);
this._navigationSlotHasChildren = this.#hasNodes(e);
this.#setSlotVisibility(e.target as HTMLElement, this._navigationSlotHasChildren);
}}></slot>
<slot
id="action-menu-slot"
name="action-menu"
@slotchange=${(e: Event) => {
this._actionsMenuSlotHasChildren = this.#hasNodes(e);
this.#setSlotVisibility(e.target as HTMLElement, this._actionsMenuSlotHasChildren);
}}></slot>
</div>
@@ -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;
}

View File

@@ -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';

View File

@@ -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`<uui-input
id="datetime"
label="Pick a date or time"
.label=${this.localize.term('placeholders_enterdate')}
.min=${this.min}
.max=${this.max}
.step=${this.step}
.type=${this.type}
.value="${this.displayValue?.replace('Z', '')}"
value=${ifDefined(this.value)}
@change=${this.#onChange}>
</uui-input>`;
}

View File

@@ -0,0 +1,3 @@
import './split-panel.element.js';
export * from './split-panel.element.js';

View File

@@ -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<any> | Map<PropertyKey, unknown>): 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`
<div id="main">
<slot
name="start"
@slotchange=${this.#onSlotChanged}
style="width: ${this._hasStartPanel ? '100%' : '0'}"></slot>
<div id="divider">
<div id="divider-touch-area" tabindex="0"></div>
</div>
<slot name="end" @slotchange=${this.#onSlotChanged} style="width: ${this._hasEndPanel ? '100%' : '0'}"></slot>
</div>
`;
}
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;
}
}

View File

@@ -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<UmbSplitPanelElement> = {
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<UmbSplitPanelElement>;
export const Overview: Story = {
render: (props) => html`
<umb-split-panel .lock=${props.lock} .snap=${props.snap} .position=${props.position}>
<div id="start" slot="start">Start</div>
<div id="end" slot="end">End</div>
</umb-split-panel>
<style>
#start,
#end {
background-color: #ffffff;
color: #383838;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 600;
}
#start {
border-right: 2px solid #e5e5e5;
min-height: 300px;
}
#end {
border-left: 2px solid #e5e5e5;
}
</style>
`,
};

View File

@@ -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`<div class="tab-actions">
return html`<div>
${this._compositionRepositoryAlias
? html`<uui-button
look="outline"
@@ -458,7 +458,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
label=${tab.name && tab.name !== '' ? tab.name : 'unnamed'}
.active=${tabActive}
href=${path}
data-umb-tab-id=${ifDefined(tab.id)}>
data-umb-tab-id=${ifDefined(tab.id)}
?sortable=${ownedTab}>
${this.renderTabInner(tab, tabActive, ownedTab)}
</uui-tab>`;
}
@@ -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 {

View File

@@ -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() {

View File

@@ -14,11 +14,21 @@ export class UmbSectionAliasCondition
{
constructor(host: UmbControllerHost, args: UmbConditionControllerArguments<SectionAliasConditionConfig>) {
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<string>;
};
export const manifest: ManifestCondition = {

View File

@@ -24,13 +24,17 @@ export class UmbConfirmModalElement extends UmbLitElement {
<uui-dialog-layout class="uui-text" .headline=${this.data?.headline || null}>
${this.data?.content}
<uui-button slot="actions" id="cancel" label="Cancel" @click="${this._handleCancel}"></uui-button>
<uui-button
slot="actions"
id="cancel"
label=${this.data?.cancelLabel || this.localize.term('buttons_confirmActionCancel')}
@click=${this._handleCancel}></uui-button>
<uui-button
slot="actions"
id="confirm"
color="${this.data?.color || 'positive'}"
color=${this.data?.color || 'positive'}
look="primary"
label="${this.data?.confirmLabel || 'Confirm'}"
label=${this.data?.confirmLabel || this.localize.term('buttons_confirmActionConfirm')}
@click=${this._handleConfirm}
${umbFocus()}></uui-button>
</uui-dialog-layout>

View File

@@ -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`
<umb-body-layout headline="Embed">
<uui-box>
<umb-property-layout label="URL" orientation="vertical">
<umb-property-layout label=${this.localize.term('general_url')} orientation="vertical">
<div slot="editor">
<uui-input .value=${this._model.url} type="text" @change=${this.#onUrlChange} required="true">
<uui-input id="url" .value=${this._url} @input=${this.#onUrlChange} required="true">
<uui-button
slot="append"
look="primary"
color="positive"
@click=${this.#getPreview}
?disabled=${!this._model.url}
label="Retrieve"></uui-button>
label=${this.localize.term('general_retrieve')}></uui-button>
</uui-input>
</div>
</umb-property-layout>
${when(
this.#embedResult?.oEmbedStatus === OEmbedStatus.Success || this._model.a11yInfo,
this.#validUrl !== undefined,
() =>
html` <umb-property-layout label="Preview" orientation="vertical">
html` <umb-property-layout label=${this.localize.term('general_preview')} orientation="vertical">
<div slot="editor">
${when(this.#loading, () => html`<uui-loader-circle></uui-loader-circle>`)}
${when(this.#embedResult?.markup, () => html`${unsafeHTML(this.#embedResult.markup)}`)}
${when(this._model.info, () => html` <p aria-hidden="true">${this._model.info}</p>`)}
${when(
this._model.a11yInfo,
() => html` <p class="sr-only" role="alert">${this._model.a11yInfo}</p>`,
)}
${when(this._loading === 'waiting', () => html`<uui-loader-circle></uui-loader-circle>`)}
${when(this.value?.markup, () => html`${unsafeHTML(this.value.markup)}`)}
</div>
</umb-property-layout>`,
)}
<umb-property-layout label="Width" orientation="vertical">
<umb-property-layout label=${this.localize.term('general_width')} orientation="vertical">
<uui-input
slot="editor"
.value=${this._model.width}
.value=${this._width}
type="number"
?disabled=${this.#dimensionControlsDisabled()}
@change=${this.#onWidthChange}></uui-input>
@change=${this.#onWidthChange}
?disabled=${this.#validUrl ? false : true}></uui-input>
</umb-property-layout>
<umb-property-layout label="Height" orientation="vertical">
<umb-property-layout label=${this.localize.term('general_height')} orientation="vertical">
<uui-input
slot="editor"
.value=${this._model.height}
.value=${this._height}
type="number"
?disabled=${this.#dimensionControlsDisabled()}
@change=${this.#onHeightChange}></uui-input>
@change=${this.#onHeightChange}
?disabled=${this.#validUrl ? false : true}></uui-input>
</umb-property-layout>
<umb-property-layout label="Constrain" orientation="vertical">
<umb-property-layout label=${this.localize.term('general_constrainProportions')} orientation="vertical">
<uui-toggle
slot="editor"
@change=${this.#onConstrainChange}
?disabled=${this.#dimensionControlsDisabled()}
.checked=${this._model.constrain}></uui-toggle>
.checked=${this.value?.constrain ?? false}></uui-toggle>
</umb-property-layout>
</uui-box>
<uui-button slot="actions" id="cancel" label="Cancel" @click=${this.#handleCancel}>Cancel</uui-button>
<uui-button
slot="actions"
id="cancel"
label=${this.localize.term('buttons_confirmActionCancel')}
@click=${() => this.modalContext?.reject()}></uui-button>
<uui-button
slot="actions"
id="submit"
color="positive"
look="primary"
label="Submit"
@click=${this.#handleConfirm}></uui-button>
label=${this.localize.term('buttons_confirmActionConfirm')}
@click=${() => this.modalContext?.submit()}></uui-button>
</umb-body-layout>
`;
}
@@ -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;
}
`,
];
}

View File

@@ -0,0 +1 @@
export * from './repository/index.js';

View File

@@ -0,0 +1,13 @@
import { manifests as repositories } from './repository/manifests.js';
import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
const modals: Array<ManifestModal> = [
{
type: 'modal',
alias: 'Umb.Modal.EmbeddedMedia',
name: 'Embedded Media Modal',
element: () => import('./embedded-media-modal.element.js'),
},
];
export const manifests = [...modals, ...repositories];

View File

@@ -0,0 +1,2 @@
export { UmbOEmbedRepository } from './oembed.repository.js';
export { UMB_OEMBED_REPOSITORY_ALIAS } from './manifests.js';

View File

@@ -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<ManifestTypes> = [repository];

View File

@@ -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 };
}
}

View File

@@ -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 }));
}
}

View File

@@ -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<UmbIconPicker
}
}
#changeIcon(e: InputEvent | KeyboardEvent) {
#changeIcon(e: InputEvent | KeyboardEvent, iconName: string) {
if (e.type == 'click' || (e.type == 'keyup' && (e as KeyboardEvent).key == 'Enter')) {
const iconName = (e.target as UUIIconElement).name;
if (iconName) {
this.modalContext?.updateValue({ icon: iconName });
}
this.modalContext?.updateValue({ icon: iconName });
}
}
@@ -142,8 +139,8 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
label="${icon.name}"
title="${icon.name}"
class="${icon.name === this._currentIcon ? 'selected' : ''}"
@click="${this.#changeIcon}"
@keyup="${this.#changeIcon}">
@click=${(e: InputEvent) => this.#changeIcon(e, icon.name)}
@keyup=${(e: KeyboardEvent) => this.#changeIcon(e, icon.name)}>
<uui-icon
style="--uui-icon-color: var(${extractUmbColorVariable(this._currentColor)})"
name="${icon.name}">

View File

@@ -0,0 +1 @@
export * from './embedded-media/index.js';

View File

@@ -5,6 +5,7 @@ export interface UmbConfirmModalData {
headline: string;
content: TemplateResult | string;
color?: 'positive' | 'danger';
cancelLabel?: string;
confirmLabel?: string;
}

View File

@@ -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<UmbEmbeddedMediaDimensionsModel> {
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<UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue>(

View File

@@ -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';

View File

@@ -44,6 +44,9 @@ export class UmbSectionDefaultElement extends UmbLitElement implements UmbSectio
UmbExtensionElementInitializer<ManifestSectionSidebarApp | ManifestSectionSidebarAppMenuKind>
>;
@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`
<!-- TODO: these extensions should be combined into one type: sectionSidebarApp with a "subtype" -->
<umb-section-sidebar>
${repeat(
this._sidebarApps,
(app) => app.alias,
(app) => app.component,
)}
</umb-section-sidebar>
`
: nothing}
<umb-section-main>
${this._routes && this._routes.length > 0
? html`<umb-router-slot id="router-slot" .routes=${this._routes}></umb-router-slot>`
<umb-split-panel
lock="start"
snap="300px"
@position-changed=${this.#onSplitPanelChange}
.position=${this._splitPanelPosition}>
${this._sidebarApps && this._sidebarApps.length > 0
? html`
<!-- TODO: these extensions should be combined into one type: sectionSidebarApp with a "subtype" -->
<umb-section-sidebar slot="start">
${repeat(
this._sidebarApps,
(app) => app.alias,
(app) => app.component,
)}
</umb-section-sidebar>
`
: nothing}
<slot></slot>
</umb-section-main>
<umb-section-main slot="end">
${this._routes && this._routes.length > 0
? html`<umb-router-slot id="router-slot" .routes=${this._routes}></umb-router-slot>`
: nothing}
<slot></slot>
</umb-section-main>
</umb-split-panel>
`;
}
@@ -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;
}
}
`,
];
}

View File

@@ -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);

View File

@@ -29,6 +29,7 @@ export class UmbSectionSidebarElement extends UmbLitElement {
display: flex;
flex-direction: column;
z-index: 10;
position: relative;
}
#scroll-container {

View File

@@ -14,11 +14,7 @@ const entityActions: Array<ManifestTypes> = [
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',

View File

@@ -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;

View File

@@ -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<UmbAppLanguageContext>
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) {

View File

@@ -69,11 +69,11 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
// TODO: make data mapper to prevent errors
const dataType: UmbLanguageDetailModel = {
entityType: UMB_LANGUAGE_ENTITY_TYPE,
fallbackIsoCode: data.fallbackIsoCode?.toLowerCase() || null,
fallbackIsoCode: data.fallbackIsoCode || null,
isDefault: data.isDefault,
isMandatory: data.isMandatory,
name: data.name,
unique: data.isoCode.toLowerCase(),
unique: data.isoCode,
};
return { data: dataType };
@@ -90,10 +90,10 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
// TODO: make data mapper to prevent errors
const requestBody: CreateLanguageRequestModel = {
fallbackIsoCode: model.fallbackIsoCode?.toLowerCase(),
fallbackIsoCode: model.fallbackIsoCode,
isDefault: model.isDefault,
isMandatory: model.isMandatory,
isoCode: model.unique.toLowerCase(),
isoCode: model.unique,
name: model.name,
};
@@ -122,7 +122,7 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
// TODO: make data mapper to prevent errors
const requestBody: UpdateLanguageRequestModel = {
fallbackIsoCode: model.fallbackIsoCode?.toLowerCase(),
fallbackIsoCode: model.fallbackIsoCode,
isDefault: model.isDefault,
isMandatory: model.isMandatory,
name: model.name,
@@ -131,7 +131,7 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
const { error } = await tryExecuteAndNotify(
this.#host,
LanguageService.putLanguageByIsoCode({
isoCode: model.unique.toLowerCase(),
isoCode: model.unique,
requestBody,
}),
);

View File

@@ -112,10 +112,11 @@ export class UmbLanguageDetailsWorkspaceViewElement extends UmbLitElement implem
<umb-property-layout label="Language">
<div slot="editor">
<!-- TODO: disable already created cultures in the select -->
<umb-input-culture-select
value=${ifDefined(this._language?.unique)}
@change=${this.#handleCultureChange}
?readonly=${this._isNew === false}></umb-input-culture-select>
${this._isNew
? html` <umb-input-culture-select
value=${ifDefined(this._language?.unique)}
@change=${this.#handleCultureChange}></umb-input-culture-select>`
: this._language?.name}
</div>
</umb-property-layout>

View File

@@ -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 };
}),

View File

@@ -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;
}

View File

@@ -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<UmbMediaCollectionItemModel> = [];
@@ -16,7 +21,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
@state()
private _selection: Array<string | null> = [];
#collectionContext?: UmbDefaultCollectionContext<UmbMediaCollectionItemModel, UmbMediaCollectionFilterModel>;
#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`<div class="container"><uui-loader></uui-loader></div>`;
}
if (this._items.length === 0) {
return html`<div class="container"><p>${this.localize.term('content_listViewNoItems')}</p></div>`;
}
return this._items.length === 0 ? this.#renderEmpty() : this.#renderItems();
}
#renderEmpty() {
if (this._items.length > 0) return nothing;
return html`
<div id="media-grid">
${repeat(
this._items,
(item) => item.unique,
(item) => this.#renderCard(item),
<div class="container">
${when(
this._loading,
() => html`<uui-loader></uui-loader>`,
() => html`<p>${this.localize.term('content_listViewNoItems')}</p>`,
)}
</div>
`;
}
#renderCard(item: UmbMediaCollectionItemModel) {
#renderItems() {
if (this._items.length === 0) return nothing;
return html`
<div id="media-grid">
${repeat(
this._items,
(item) => item.unique,
(item) => this.#renderItem(item),
)}
</div>
${when(this._loading, () => html`<uui-loader-bar></uui-loader-bar>`)}
`;
}
#renderItem(item: UmbMediaCollectionItemModel) {
// TODO: Fix the file extension when media items have a file extension. [?]
return html`
<uui-card-media
@@ -87,7 +123,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement {
selectable
?select-only=${this._selection && this._selection.length > 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"

View File

@@ -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`<uui-button
look="default"
color="default"
compact
href="${this._editMediaPath}edit/${this.value.unique}"
label="${this.value.name}"
@click=${this.#onClick}></uui-button>`;
if (!this.value) return nothing;
return html`
<uui-button
compact
href=${this.value.editPath}
label=${this.value.item.name}
@click=${this.#onClick}></uui-button>
`;
}
static styles = [

View File

@@ -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<string> = [];
@state()
private _skip: number = 0;
#collectionContext?: UmbDefaultCollectionContext<UmbMediaCollectionItemModel, UmbMediaCollectionFilterModel>;
#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`<div class="container"><uui-loader></uui-loader></div>`;
}
return this._tableItems.length === 0 ? this.#renderEmpty() : this.#renderItems();
}
if (this._tableItems.length === 0) {
return html`<div class="container"><p>${this.localize.term('content_listViewNoItems')}</p></div>`;
}
#renderEmpty() {
if (this._tableItems.length > 0) return nothing;
return html`
<div class="container">
${when(
this._loading,
() => html`<uui-loader></uui-loader>`,
() => html`<p>${this.localize.term('content_listViewNoItems')}</p>`,
)}
</div>
`;
}
#renderItems() {
if (this._tableItems.length === 0) return nothing;
return html`
<umb-table
.config=${this._tableConfig}
.columns=${this._tableColumns}
.items=${this._tableItems}
.selection=${this._selection}
@selected="${this.#handleSelect}"
@deselected="${this.#handleDeselect}"
@ordered="${this.#handleOrdering}"></umb-table>
@selected=${this.#handleSelect}
@deselected=${this.#handleDeselect}
@ordered=${this.#handleOrdering}></umb-table>
${when(this._loading, () => html`<uui-loader-bar></uui-loader-bar>`)}
`;
}

View File

@@ -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';

View File

@@ -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');

View File

@@ -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
</div>
<div class="general-item">
<umb-localize class="headline" key="template_id"></umb-localize>
<span>${this._memberTypeUnique}</span>
<span>${this._unique}</span>
</div>
`;
}

View File

@@ -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<Option> = [
{ value: 'name', name: 'Name' },
{ value: 'updateDate', name: 'Last edited' },
{ value: 'owner', name: 'Created by' },
];
private _options: Array<Option> = [];
@property({ type: Object, attribute: false })
public config?: UmbPropertyEditorConfigCollection;
constructor() {
super();
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (instance) => {
const workspace = instance;
this.observe(
await workspace.propertyValueByAlias<Array<UmbCollectionColumnConfiguration>>('includeProperties'),
(includeProperties) => {
if (!includeProperties) return;
this._options = includeProperties.map((property) => ({
name: property.header,
value: property.alias,
selected: property.alias === this.value,
}));
},
'_observeIncludeProperties',
);
});
}
#onChange(e: UUISelectEvent) {
this.value = e.target.value as string;
@@ -36,6 +46,7 @@ export class UmbPropertyEditorUICollectionOrderByElement extends UmbLitElement i
}
render() {
if (!this._options.length) return html`<p><em>Add a column (above) to order by.</em></p>`;
return html`<uui-select label="select" .options=${this._options} @change=${this.#onChange}></uui-select>`;
}
}

View File

@@ -23,13 +23,6 @@ const propertyEditorUiManifest: ManifestPropertyEditorUi = {
description: 'The properties that will be displayed for each column.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Collection.LayoutConfiguration',
},
{
alias: 'pageSize',
label: 'Page Size',
description: 'Number of items per page.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Number',
config: [{ alias: 'min', value: 0 }],
},
{
alias: 'orderBy',
label: 'Order By',
@@ -41,6 +34,13 @@ const propertyEditorUiManifest: ManifestPropertyEditorUi = {
label: 'Order Direction',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.OrderDirection',
},
{
alias: 'pageSize',
label: 'Page Size',
description: 'Number of items per page.',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Number',
config: [{ alias: 'min', value: 0 }],
},
{
alias: 'bulkActionPermissions',
label: 'Bulk Action Permissions',

View File

@@ -6,6 +6,23 @@ import type { UmbInputDateElement } from '@umbraco-cms/backoffice/components';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
/**
* This property editor allows the user to pick a date, datetime-local, or time.
* It uses raw datetime strings back and forth, and therefore it has no knowledge
* of timezones. It uses a regular HTML input element underneath, which supports the known
* definitions of "date", "datetime-local", and "time".
*
* The underlying input element reports the value differently depending on the type configuration. Here
* are some examples from the change event:
*
* date: 2024-05-03
* datetime-local: 2024-05-03T10:44
* time: 10:44
*
* These values are approximately similar to what Umbraco expects for the Umbraco.DateTime
* data editor with one exception: the "T" character in "datetime-local". To be backwards compatible, we are
* replacing the T character with a whitespace, which also happens to work just fine
* with the "datetime-local" type.
*
* @element umb-property-editor-ui-date-picker
*/
@customElement('umb-property-editor-ui-date-picker')
@@ -23,28 +40,21 @@ export class UmbPropertyEditorUIDatePickerElement extends UmbLitElement implemen
private _step?: number;
@property()
set value(value: string | undefined) {
if (value) {
// NOTE: If the `value` contains a space, then it doesn't contain the timezone, so may not be parsed as UTC. [LK]
const datetime = !value.includes(' ') ? value : value + ' +00';
this.#value = new Date(datetime).toJSON();
}
}
get value() {
return this.#value;
}
#value?: string;
value?: string;
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
const oldVal = this._inputType;
// Format string prevalue/config
const format = config.getValueByAlias<string>('format');
const hasTime = format?.includes('H') || format?.includes('m');
const hasSeconds = format?.includes('s');
this._inputType = hasTime ? 'datetime-local' : 'date';
// Based on the type of format string change the UUI-input type
// Note: The format string is not validated, so it's possible to have an invalid format string,
// but we do not use the format string for anything else than to determine the input type.
// The format string is not used to validate the value and is only used on the frontend.
const timeFormatPattern = /^h{1,2}:m{1,2}(:s{1,2})?\s?a?$/gim;
if (format?.toLowerCase().match(timeFormatPattern)) {
this._inputType = 'time';
@@ -52,14 +62,46 @@ export class UmbPropertyEditorUIDatePickerElement extends UmbLitElement implemen
this._min = config.getValueByAlias('min');
this._max = config.getValueByAlias('max');
this._step = config.getValueByAlias('step');
this._step = config.getValueByAlias('step') ?? hasSeconds ? 1 : undefined;
this.requestUpdate('_inputType', oldVal);
if (this.value) {
this.#formatValue(this.value);
}
}
#onChange(event: CustomEvent & { target: HTMLInputElement }) {
this.value = event.target.value;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
#onChange(event: CustomEvent & { target: UmbInputDateElement }) {
this.#formatValue(event.target.value.toString());
}
/**
* Formats the value depending on the input type.
*/
#formatValue(value: string) {
// Check that the value is a valid date
const valueToDate = new Date(value);
if (isNaN(valueToDate.getTime())) {
console.warn('[Umbraco.DatePicker] The value is not a valid date.', value);
return;
}
// Replace the potential time demoninator 'T' with a whitespace for backwards compatibility
value = value.replace('T', ' ');
// If the inputType is 'date', we need to make sure the value doesn't have a time
if (this._inputType === 'date' && value.includes(' ')) {
value = value.split(' ')[0];
}
// If the inputType is 'time', we need to remove the date part of the value
if (this._inputType === 'time' && value.includes(' ')) {
value = value.split(' ')[1];
}
const valueHasChanged = this.value !== value;
if (valueHasChanged) {
this.value = value;
this.dispatchEvent(new UmbPropertyValueChangeEvent());
}
}
render() {

View File

@@ -21,7 +21,8 @@ describe('UmbPropertyEditorUIDatePickerElement', () => {
expect(inputElement).to.exist;
});
it('should show a datetime-local input by default', () => {
it('should show a datetime-local input by default', async () => {
await element.updateComplete;
expect(inputElement.type).to.equal('datetime-local');
});

View File

@@ -1 +1,2 @@
import './checkbox-list/components/index.js';
import './content-picker/components/index.js';

View File

@@ -6,7 +6,7 @@ export const manifests: Array<ManifestTypes> = [
name: 'Integer',
alias: 'Umbraco.Integer',
meta: {
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Integer',
defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Number',
settings: {
properties: [
{

View File

@@ -2,35 +2,7 @@ import { manifests as decimalSchemaManifests } from './Umbraco.Decimal.js';
import { manifests as integerSchemaManifests } from './Umbraco.Integer.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
// TODO: we don't really want this config value to be changed from the UI. We need a way to handle hidden config properties.
const allowDecimalsConfig = {
alias: 'allowDecimals',
label: 'Allow decimals',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle',
};
export const manifests: Array<ManifestTypes> = [
{
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.Integer',
name: 'Integer Property Editor UI',
element: () => import('./property-editor-ui-number.element.js'),
meta: {
label: 'Integer',
propertyEditorSchemaAlias: 'Umbraco.Integer',
icon: 'icon-autofill',
group: 'common',
settings: {
properties: [allowDecimalsConfig],
defaultData: [
{
alias: 'allowDecimals',
value: false,
},
],
},
},
},
{
type: 'propertyEditorUi',
alias: 'Umb.PropertyEditorUi.Decimal',
@@ -42,12 +14,8 @@ export const manifests: Array<ManifestTypes> = [
icon: 'icon-autofill',
group: 'common',
settings: {
properties: [allowDecimalsConfig],
properties: [],
defaultData: [
{
alias: 'allowDecimals',
value: true,
},
{
alias: 'step',
value: '0.01',

View File

@@ -0,0 +1 @@
export * from './modal/index.js';

View File

@@ -0,0 +1,5 @@
import { manifests as modalManifests } from './modal/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestTypes> = [...modalManifests];

View File

@@ -0,0 +1,76 @@
import type {
UmbExamineFieldsSettingsModalData,
UmbExamineFieldsSettingsModalValue,
UmbExamineFieldSettingsType,
} from './examine-fields-settings-modal.token.js';
import { html, css, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
@customElement('umb-examine-fields-settings-modal')
export class UmbExamineFieldsSettingsModalElement extends UmbModalBaseElement<
UmbExamineFieldsSettingsModalData,
UmbExamineFieldsSettingsModalValue
> {
render() {
return html`<umb-body-layout headline=${this.localize.term('examineManagement_fields')}>
<uui-scroll-container id="field-settings"> ${this.#renderFields()} </uui-scroll-container>
<div slot="actions">
<uui-button
look="primary"
label=${this.localize.term('general_close')}
@click="${this._submitModal}"></uui-button>
</div>
</umb-body-layout>`;
}
#setExposed(fieldSetting: UmbExamineFieldSettingsType) {
const newField: UmbExamineFieldSettingsType = { ...fieldSetting, exposed: !fieldSetting.exposed };
const updatedFields =
this.modalContext?.getValue().fields.map((field) => {
if (field.name === fieldSetting.name) return newField;
else return field;
}) ?? [];
this.modalContext?.updateValue({ fields: updatedFields });
}
#renderFields() {
if (!this.value.fields.length) return;
return html`<span>
${Object.values(this.value.fields).map((field) => {
return html`<uui-toggle
name="${field.name}"
label="${field.name}"
.checked="${field.exposed}"
@change="${() => this.#setExposed(field)}"></uui-toggle>
<br />`;
})}
</span>`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: relative;
}
uui-scroll-container {
overflow-y: scroll;
max-height: 100%;
min-height: 0;
flex: 1;
}
`,
];
}
export default UmbExamineFieldsSettingsModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-examine-fields-settings-modal': UmbExamineFieldsSettingsModalElement;
}
}

View File

@@ -1,20 +1,20 @@
import { UmbModalToken } from './modal-token.js';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export type UmbExamineFieldsSettingsModalData = never;
type FieldSettingsType = {
export type UmbExamineFieldSettingsType = {
name: string;
exposed: boolean;
};
export type UmbExamineFieldsSettingsModalValue = {
fields: Array<FieldSettingsType>;
fields: Array<UmbExamineFieldSettingsType>;
};
export const UMB_EXAMINE_FIELDS_SETTINGS_MODAL = new UmbModalToken<
UmbExamineFieldsSettingsModalData,
UmbExamineFieldsSettingsModalValue
>('Umb.Modal.ExamineFieldsSettings', {
>('Umb.Modal.Examine.FieldsSettings', {
modal: {
type: 'sidebar',
size: 'small',

View File

@@ -0,0 +1,2 @@
export * from './examine-fields-settings-modal.element.js';
export * from './examine-fields-settings-modal.token.js';

View File

@@ -1,11 +1,15 @@
import type {
UmbExamineFieldsViewerModalData,
UmbExamineFieldsViewerModalValue,
} from './examine-fields-viewer-modal.token.js';
import { html, css, nothing, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import type { SearchResultResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
@customElement('umb-modal-element-fields-viewer')
export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
SearchResultResponseModel & { name: string }
@customElement('umb-examine-fields-viewer-modal')
export class UmbExamineFieldsViewerModalElement extends UmbModalBaseElement<
UmbExamineFieldsViewerModalData,
UmbExamineFieldsViewerModalValue
> {
private _handleClose() {
this.modalContext?.reject();
@@ -15,7 +19,7 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
if (!this.data) return nothing;
return html`
<uui-dialog-layout class="uui-text" headline="${this.data.name}">
<umb-body-layout headline="${this.data?.name}">
<uui-scroll-container id="field-viewer">
<span>
<uui-table>
@@ -23,7 +27,7 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
<uui-table-head-cell> Field </uui-table-head-cell>
<uui-table-head-cell> Value </uui-table-head-cell>
</uui-table-head>
${Object.values(this.data.fields ?? []).map((cell) => {
${Object.values(this.data.searchResult.fields ?? []).map((cell) => {
return html`<uui-table-row>
<uui-table-cell> ${cell.name} </uui-table-cell>
<uui-table-cell> ${cell.values?.join(', ')} </uui-table-cell>
@@ -32,10 +36,13 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
</uui-table>
</span>
</uui-scroll-container>
<div>
<uui-button look="primary" @click="${this._handleClose}">Close</uui-button>
<div slot="actions">
<uui-button
look="primary"
label=${this.localize.term('general_close')}
@click=${this._rejectModal}></uui-button>
</div>
</uui-dialog-layout>
</umb-body-layout>
`;
}
@@ -45,11 +52,6 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
:host {
display: relative;
}
uui-dialog-layout {
display: flex;
flex-direction: column;
height: 100%;
}
span {
display: block;
@@ -57,22 +59,18 @@ export class UmbModalElementFieldsViewerElement extends UmbModalBaseElement<
}
uui-scroll-container {
line-height: 0;
overflow-y: scroll;
max-height: 100%;
min-height: 0;
}
div {
margin-top: var(--uui-size-space-5);
display: flex;
flex-direction: row-reverse;
}
`,
];
}
export default UmbExamineFieldsViewerModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-modal-element-fields-viewer': UmbModalElementFieldsViewerElement;
'umb-examine-fields-viewer-modal': UmbExamineFieldsViewerModalElement;
}
}

View File

@@ -0,0 +1,19 @@
import type { SearchResultResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export type UmbExamineFieldsViewerModalData = {
name: string;
searchResult: SearchResultResponseModel;
};
export type UmbExamineFieldsViewerModalValue = never;
export const UMB_EXAMINE_FIELDS_VIEWER_MODAL = new UmbModalToken<
UmbExamineFieldsViewerModalData,
UmbExamineFieldsViewerModalValue
>('Umb.Modal.Examine.FieldsViewer', {
modal: {
type: 'sidebar',
size: 'small',
},
});

View File

@@ -0,0 +1,2 @@
export * from './examine-fields-viewer-modal.element.js';
export * from './examine-fields-viewer-modal.token.js';

View File

@@ -0,0 +1,2 @@
export * from './fields-settings/index.js';
export * from './fields-viewer/index.js';

View File

@@ -0,0 +1,18 @@
import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
const modals: Array<ManifestModal> = [
{
type: 'modal',
alias: 'Umb.Modal.Examine.FieldsSettings',
name: 'Examine Field Settings Modal',
js: () => import('./fields-settings/examine-fields-settings-modal.element.js'),
},
{
type: 'modal',
alias: 'Umb.Modal.Examine.FieldsViewer',
name: 'Examine Field Viewer Modal',
js: () => import('./fields-viewer/examine-fields-viewer-modal.element.js'),
},
];
export const manifests: Array<ManifestTypes> = [...modals];

View File

@@ -1,81 +0,0 @@
import { html, css, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type {
UmbExamineFieldsSettingsModalValue,
UmbExamineFieldsSettingsModalData} from '@umbraco-cms/backoffice/modal';
import {
UmbModalBaseElement,
} from '@umbraco-cms/backoffice/modal';
@customElement('umb-examine-fields-settings-modal')
export default class UmbExamineFieldsSettingsModalElement extends UmbModalBaseElement<
UmbExamineFieldsSettingsModalData,
UmbExamineFieldsSettingsModalValue
> {
render() {
if (this.value.fields) {
return html`
<uui-dialog-layout headline="Show fields">
<uui-scroll-container id="field-settings">
<span>
${Object.values(this.value.fields).map((field, index) => {
return html`<uui-toggle
name="${field.name}"
label="${field.name}"
.checked="${field.exposed}"
@change="${() => {
this.value.fields ? (this.value.fields[index].exposed = !field.exposed) : '';
}}"></uui-toggle>
<br />`;
})}
</span>
</uui-scroll-container>
<div>
<uui-button look="primary" label="Close sidebar" @click="${this._submitModal}">Close</uui-button>
</div>
</uui-dialog-layout>
`;
} else {
return '';
}
}
static styles = [
UmbTextStyles,
css`
:host {
display: relative;
}
uui-dialog-layout {
display: flex;
flex-direction: column;
height: 100%;
background-color: var(--uui-color-surface);
box-shadow: var(--uui-shadow-depth-1, 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24));
border-radius: var(--uui-border-radius);
padding: var(--uui-size-space-5);
box-sizing: border-box;
}
uui-scroll-container {
overflow-y: scroll;
max-height: 100%;
min-height: 0;
flex: 1;
}
div {
margin-top: var(--uui-size-space-5);
display: flex;
flex-direction: row-reverse;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-examine-fields-settings-modal': UmbExamineFieldsSettingsModalElement;
}
}

View File

@@ -1,7 +1,7 @@
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
import { css, html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { IndexResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { HealthStatusResponseModel, IndexResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { HealthStatusModel, IndexerService } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
@@ -25,35 +25,51 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
connectedCallback() {
super.connectedCallback();
this._getIndexData();
this.#loadData();
}
private async _getIndexData() {
async #loadData() {
this._indexData = await this.#getIndexData();
if (this._indexData?.healthStatus.status === HealthStatusModel.REBUILDING) {
this._buttonState = 'waiting';
this._continuousPolling();
} else {
this._loading = false;
}
}
async #getIndexData() {
const { data } = await tryExecuteAndNotify(
this,
IndexerService.getIndexerByIndexName({ indexName: this.indexName }),
);
this._indexData = data;
return data;
}
// TODO: Add continuous polling to update the status
if (this._indexData?.healthStatus === HealthStatusModel.REBUILDING) {
this._buttonState = 'waiting';
private async _continuousPolling() {
//Checking the server every 5 seconds to see if the index is still rebuilding.
while (this._buttonState === 'waiting') {
await new Promise((resolve) => setTimeout(resolve, 5000));
this._indexData = await this.#getIndexData();
if (this._indexData?.healthStatus.status !== HealthStatusModel.REBUILDING) {
this._buttonState = 'success';
}
}
this._loading = false;
return;
}
private async _onRebuildHandler() {
await umbConfirmModal(this, {
headline: `Rebuild ${this.indexName}`,
content: html`
This will cause the index to be rebuilt.<br />
headline: `${this.localize.term('examineManagement_rebuildIndex')} ${this.indexName}`,
content: html`<umb-localize key="examineManagement_rebuildIndexWarning"
>This will cause the index to be rebuilt.<br />
Depending on how much content there is in your site this could take a while.<br />
It is not recommended to rebuild an index during times of high website traffic or when editors are editing
content.
`,
content.</umb-localize
> `,
color: 'danger',
confirmLabel: 'Rebuild',
confirmLabel: this.localize.term('examineManagement_rebuildIndex'),
});
this._rebuild();
@@ -68,9 +84,22 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
this._buttonState = 'failed';
return;
}
this._buttonState = 'success';
await this._getIndexData();
await this.#loadData();
}
#renderHealthStatus(healthStatus: HealthStatusResponseModel) {
const msg = healthStatus.message ? healthStatus.message : healthStatus.status;
switch (healthStatus.status) {
case HealthStatusModel.HEALTHY:
return html`<umb-icon name="icon-check color-green"></umb-icon>${msg}`;
case HealthStatusModel.UNHEALTHY:
return html`<umb-icon name="icon-error color-red"></umb-icon>${msg}`;
case HealthStatusModel.REBUILDING:
return html`<umb-icon name="icon-time color-yellow"></umb-icon>${msg}`;
default:
return;
}
}
render() {
@@ -79,43 +108,41 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
return html`
<uui-box headline="${this.indexName}">
<p>
<strong>Health Status</strong><br />
The health status of the ${this.indexName} and if it can be read
<strong><umb-localize key="examineManagement_healthStatus">Health Status</umb-localize></strong
><br />
<umb-localize key="examineManagement_healthStatusDescription"
>The health status of the ${this.indexName} and if it can be read</umb-localize
>
</p>
<div>
<uui-icon-essentials>
${
this._indexData.healthStatus === HealthStatusModel.UNHEALTHY
? html`<uui-icon name="wrong" class="danger"></uui-icon>`
: html`<uui-icon name="check" class="positive"></uui-icon>`
}
</uui-icon>
</uui-icon-essentials>
${this._indexData.healthStatus}
</div>
<div id="health-status">${this.#renderHealthStatus(this._indexData.healthStatus)}</div>
</uui-box>
${this.renderIndexSearch()} ${this.renderPropertyList()} ${this.renderTools()}
`;
}
private renderIndexSearch() {
if (!this._indexData || this._indexData.healthStatus !== HealthStatusModel.HEALTHY) return nothing;
// Do we want to show the search while rebuilding?
if (!this._indexData || this._indexData.healthStatus.status === HealthStatusModel.REBUILDING) return nothing;
return html`<umb-dashboard-examine-searcher .searcherName="${this.indexName}"></umb-dashboard-examine-searcher>`;
}
private renderPropertyList() {
if (!this._indexData) return nothing;
return html`<uui-box headline="Index info">
<p>Lists the properties of the ${this.indexName}</p>
return html`<uui-box headline=${this.localize.term('examineManagement_indexInfo')}>
<p>
<umb-localize key="examineManagement_indexInfoDescription"
>Lists the properties of the ${this.indexName}</umb-localize
>
</p>
<uui-table class="info">
<uui-table-row>
<uui-table-cell style="width:0px; font-weight: bold;"> documentCount </uui-table-cell>
<uui-table-cell>${this._indexData.documentCount} </uui-table-cell>
<uui-table-cell style="width:0px; font-weight: bold;">DocumentCount</uui-table-cell>
<uui-table-cell>${this._indexData.documentCount}</uui-table-cell>
</uui-table-row>
<uui-table-row>
<uui-table-cell style="width:0px; font-weight: bold;"> fieldCount </uui-table-cell>
<uui-table-cell>${this._indexData.fieldCount} </uui-table-cell>
<uui-table-cell style="width:0px; font-weight: bold;">FieldCount</uui-table-cell>
<uui-table-cell>${this._indexData.fieldCount}</uui-table-cell>
</uui-table-row>
${this._indexData.providerProperties
? Object.entries(this._indexData.providerProperties).map((entry) => {
@@ -130,23 +157,26 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
}
private renderTools() {
return html` <uui-box headline="Tools">
<p>Tools to manage the ${this.indexName}</p>
return html` <uui-box headline=${this.localize.term('examineManagement_tools')}>
<p><umb-localize key="examineManagement_toolsDescription">Tools to manage the ${this.indexName}</umb-localize></p>
<uui-button
color="danger"
look="primary"
.state="${this._buttonState}"
@click="${this._onRebuildHandler}"
.disabled="${this._indexData?.canRebuild ? false : true}"
label="Rebuild index">
Rebuild
</uui-button>
label=${this.localize.term('examineManagement_rebuildIndex')}></uui-button>
</uui-box>`;
}
static styles = [
UmbTextStyles,
css`
#health-status {
display: flex;
gap: var(--uui-size-6);
}
:host {
display: block;
}
@@ -190,13 +220,6 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
padding-right: var(--uui-size-space-5);
}
.positive {
color: var(--uui-color-positive);
}
.danger {
color: var(--uui-color-danger);
}
button {
background: none;
border: none;

View File

@@ -40,19 +40,38 @@ export class UmbDashboardExamineOverviewElement extends UmbLitElement {
this._loadingSearchers = false;
}
#renderStatus(status: HealthStatusModel) {
switch (status) {
case HealthStatusModel.HEALTHY:
return html`<umb-icon name="icon-check color-green"></umb-icon>`;
case HealthStatusModel.UNHEALTHY:
return html`<umb-icon name="icon-error color-red"></umb-icon>`;
case HealthStatusModel.REBUILDING:
return html`<umb-icon name="icon-time color-yellow"></umb-icon>`;
default:
return;
}
}
render() {
return html`
<uui-box headline="Indexers" class="overview">
<uui-box headline=${this.localize.term('examineManagement_indexers')} class="overview">
<p>
<strong>Manage Examine's indexes</strong><br />
Allows you to view the details of each index and provides some tools for managing the indexes
<strong><umb-localize key="examineManagement_manageIndexes">Manage Examine's indexes</umb-localize></strong
><br />
<umb-localize key="examineManagement_manageIndexesDescription"
>Allows you to view the details of each index and provides some tools for managing the indexes</umb-localize
>
</p>
${this.renderIndexersList()}
</uui-box>
<uui-box headline="Searchers">
<uui-box headline=${this.localize.term('examineManagement_searchers')}>
<p>
<strong>Configured Searchers</strong><br />
Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)
<strong><umb-localize key="examineManagement_configuredSearchers">Configured Searchers</umb-localize></strong
><br />
<umb-localize key="examineManagement_configuredSearchersDescription"
>Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher)</umb-localize
>
</p>
${this.renderSearchersList()}
</uui-box>
@@ -66,16 +85,7 @@ export class UmbDashboardExamineOverviewElement extends UmbLitElement {
${this._indexers.map((index) => {
return html`
<uui-table-row>
<uui-table-cell style="width:0px">
<uui-icon-essentials>
${
index.healthStatus === HealthStatusModel.UNHEALTHY
? html`<uui-icon name="wrong" class="danger"></uui-icon>`
: html`<uui-icon name="check" class="positive"></uui-icon>`
}
</uui-icon>
</uui-icon-essentials>
</uui-table-cell>
<uui-table-cell style="width:0px"> ${this.#renderStatus(index.healthStatus.status)} </uui-table-cell>
<uui-table-cell>
<a href="${window.location.href.replace(/\/+$/, '')}/index/${index.name}">${index.name}</a>
</uui-table-cell>

View File

@@ -1,14 +1,16 @@
import { UMB_EXAMINE_FIELDS_SETTINGS_MODAL, UMB_EXAMINE_FIELDS_VIEWER_MODAL } from '../modal/index.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state, query, property } from '@umbraco-cms/backoffice/external/lit';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_EXAMINE_FIELDS_SETTINGS_MODAL } from '@umbraco-cms/backoffice/modal';
import {
UMB_MODAL_MANAGER_CONTEXT,
UMB_WORKSPACE_MODAL,
UmbModalRouteRegistrationController,
} from '@umbraco-cms/backoffice/modal';
import type { SearchResultResponseModel, FieldPresentationModel } from '@umbraco-cms/backoffice/external/backend-api';
import { SearcherService } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import './modal-views/fields-viewer.element.js';
import './modal-views/fields-settings-modal.element.js';
interface ExposedSearchResultField {
name: string;
exposed: boolean;
@@ -31,15 +33,27 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
@query('#search-input')
private _searchInput!: HTMLInputElement;
private _onNameClick() {
// TODO:
alert('TODO: Open workspace for ' + this.searcherName);
}
@state()
private _workspacePath = 'aa';
private _onKeyPress(e: KeyboardEvent) {
e.key == 'Enter' ? this._onSearch() : undefined;
}
#entityType = '';
constructor() {
super();
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
.addAdditionalPath(':entityType')
.onSetup((routingInfo) => {
return { data: { entityType: routingInfo.entityType, preset: {} } };
})
.observeRouteBuilder((routeBuilder) => {
this._workspacePath = routeBuilder({ entityType: this.#entityType });
});
}
private async _onSearch() {
if (!this._searchInput.value.length) return;
this._searchLoading = true;
@@ -86,36 +100,48 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
const modalContext = modalManager.open(this, UMB_EXAMINE_FIELDS_SETTINGS_MODAL, {
value: { fields: this._exposedFields ?? [] },
});
modalContext?.onSubmit().then((value) => {
this._exposedFields = value.fields;
});
await modalContext.onSubmit().catch(() => undefined);
const value = modalContext.getValue();
this._exposedFields = value?.fields;
}
async #onFieldViewClick(rowData: SearchResultResponseModel) {
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
modalManager.open(this, 'umb-modal-element-fields-viewer', {
const modalContext = modalManager.open(this, UMB_EXAMINE_FIELDS_VIEWER_MODAL, {
modal: {
type: 'sidebar',
size: 'medium',
},
data: { ...rowData, name: this.getSearchResultNodeName(rowData) },
data: { searchResult: rowData, name: this.getSearchResultNodeName(rowData) },
});
await modalContext.onSubmit().catch(() => undefined);
}
render() {
return html`
<uui-box headline="Search">
<p>Search the ${this.searcherName} and view the results</p>
<uui-box headline=${this.localize.term('general_search')}>
<p>
<umb-localize key="examineManagement_searchDescription"
>Search the ${this.searcherName} and view the results</umb-localize
>
</p>
<div class="flex">
<uui-input
type="search"
id="search-input"
placeholder="Type to filter..."
label="Type to filter"
placeholder=${this.localize.term('placeholders_filter')}
label=${this.localize.term('placeholders_filter')}
@keypress=${this._onKeyPress}
${umbFocus()}>
</uui-input>
<uui-button color="positive" look="primary" label="Search" @click="${this._onSearch}"> Search </uui-button>
<uui-button
color="positive"
look="primary"
label=${this.localize.term('general_search')}
@click="${this._onSearch}"></uui-button>
</div>
${this.renderSearchResults()}
</uui-box>
@@ -128,28 +154,44 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
return nodeNameField?.values?.join(', ') ?? '';
}
#getEntityTypeFromIndexType(indexType: string) {
switch (indexType) {
case 'content':
return 'document';
default:
return indexType;
}
}
private renderSearchResults() {
if (this._searchLoading) return html`<uui-loader></uui-loader>`;
if (!this._searchResults) return nothing;
if (!this._searchResults.length) {
return html`<p>No results found</p>`;
return html`<p>${this.localize.term('examineManagement_noResults')}</p>`;
}
return html`<div class="table-container">
<uui-scroll-container>
<uui-table class="search">
<uui-table-head>
<uui-table-head-cell style="width:0">Score</uui-table-head-cell>
<uui-table-head-cell style="width:0">Id</uui-table-head-cell>
<uui-table-head-cell>Name</uui-table-head-cell>
<uui-table-head-cell>Fields</uui-table-head-cell>
<uui-table-head-cell style="width:0">${this.localize.term('general_id')}</uui-table-head-cell>
<uui-table-head-cell>${this.localize.term('general_name')}</uui-table-head-cell>
<uui-table-head-cell>${this.localize.term('examineManagement_fields')}</uui-table-head-cell>
${this.renderHeadCells()}
</uui-table-head>
${this._searchResults?.map((rowData) => {
const indexType = rowData.fields?.find((field) => field.name === '__IndexType')?.values?.join(', ') ?? '';
this.#entityType = this.#getEntityTypeFromIndexType(indexType);
const unique = rowData.fields?.find((field) => field.name === '__Key')?.values?.join(', ') ?? '';
return html`<uui-table-row>
<uui-table-cell> ${rowData.score} </uui-table-cell>
<uui-table-cell> ${rowData.id} </uui-table-cell>
<uui-table-cell>
<uui-button look="secondary" label="Open workspace for this document" @click="${this._onNameClick}">
<uui-button
look="secondary"
label=${this.localize.term('actions_editContent')}
href=${this._workspacePath + this.#entityType + '/edit/' + unique}>
${this.getSearchResultNodeName(rowData)}
</uui-button>
</uui-table-cell>
@@ -157,9 +199,10 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
<uui-button
class="bright"
look="secondary"
label="Open sidebar to see all fields"
label=${this.localize.term('examineManagement_fieldValues')}
@click=${() => this.#onFieldViewClick(rowData)}>
${rowData.fields ? Object.keys(rowData.fields).length : ''} fields
${rowData.fields ? Object.keys(rowData.fields).length : ''}
${this.localize.term('examineManagement_fields')}
</uui-button>
</uui-table-cell>
${rowData.fields ? this.renderBodyCells(rowData.fields) : ''}
@@ -185,7 +228,7 @@ export class UmbDashboardExamineSearcherElement extends UmbLitElement {
<span>${field.name}</span>
<uui-button
look="secondary"
label="Close field ${field.name}"
label="${this.localize.term('actions_remove')} ${field.name}"
compact
@click="${() => {
this._exposedFields = this._exposedFields?.map((f) => {

View File

@@ -1,5 +1,8 @@
import { manifests as examineManifests } from './examine-management-dashboard/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
import './examine-management-dashboard/index.js';
export const manifests: Array<ManifestTypes> = [
{
type: 'headerApp',
@@ -37,10 +40,5 @@ export const manifests: Array<ManifestTypes> = [
},
],
},
{
type: 'modal',
alias: 'Umb.Modal.ExamineFieldsSettings',
name: 'Examine Field Settings Modal',
js: () => import('./examine-management-dashboard/views/modal-views/fields-settings-modal.element.js'),
},
...examineManifests,
];

View File

@@ -6,10 +6,22 @@ export default class UmbTinyMceEmbeddedMediaPlugin extends UmbTinyMcePluginBase
constructor(args: TinyMcePluginArguments) {
super(args);
this.editor.ui.registry.addButton('umbembeddialog', {
this.editor.ui.registry.addToggleButton('umbembeddialog', {
icon: 'embed',
tooltip: 'Embed',
onAction: () => this.#onAction(),
onSetup: (api) => {
const editor = this.editor;
const onNodeChange = () => {
const selectedElm = editor.selection.getNode();
api.setActive(
selectedElm.nodeName.toUpperCase() === 'DIV' && selectedElm.classList.contains('umb-embed-holder'),
);
};
editor.on('NodeChange', onNodeChange);
return () => editor.off('NodeChange', onNodeChange);
},
});
}
@@ -44,17 +56,18 @@ export default class UmbTinyMceEmbeddedMediaPlugin extends UmbTinyMcePluginBase
#insertInEditor(embed: UmbEmbeddedMediaModalValue, activeElement: HTMLElement) {
// Wrap HTML preview content here in a DIV with non-editable class of .mceNonEditable
// This turns it into a selectable/cutable block to move about
const wrapper = this.editor.dom.create(
'div',
{
class: 'mceNonEditable umb-embed-holder',
'data-embed-url': embed.url ?? '',
'data-embed-height': embed.height,
'data-embed-width': embed.width,
'data-embed-height': embed.height!,
'data-embed-width': embed.width!,
'data-embed-constrain': embed.constrain ?? false,
contenteditable: false,
},
embed.preview,
embed.markup,
);
// Only replace if activeElement is an Embed element.

View File

@@ -81,16 +81,16 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
const avatarUrls = [
{
scale: '1x',
url: user.avatarUrls?.[0],
},
{
scale: '2x',
url: user.avatarUrls?.[1],
},
{
scale: '3x',
scale: '2x',
url: user.avatarUrls?.[2],
},
{
scale: '3x',
url: user.avatarUrls?.[3],
},
];
let avatarSrcset = '';
@@ -111,6 +111,7 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
${this.#renderUserTag(user)} ${this.#renderUserGroupNames(user)} ${this.#renderUserLoginDate(user)}
<uui-avatar
style="font-size: 1.6rem;"
slot="avatar"
.name=${user.name || 'Unknown'}
img-src=${ifDefined(user.avatarUrls.length > 0 ? avatarUrls[0].url : undefined)}

View File

@@ -24,32 +24,32 @@ export class UmbWebhookCollectionServerDataSource implements UmbWebhookCollectio
}
/**
* Gets the Wwbhook collection filtered by the given filter.
* Gets the Webhook collection filtered by the given filter.
* @param {UmbWebhookCollectionFilterModel} filter
* @return {*}
* @memberof UmbWebhookCollectionServerDataSource
*/
async getCollection(_filter: UmbWebhookCollectionFilterModel) {
const { data, error } = await tryExecuteAndNotify(this.#host, WebhookService.getItemWebhook({}));
const { data, error } = await tryExecuteAndNotify(this.#host, WebhookService.getWebhook(_filter));
if (data) {
const items = data.map((item) => {
const model: UmbWebhookDetailModel = {
entityType: UMB_WEBHOOK_ENTITY_TYPE,
unique: item.url,
name: item.name,
url: item.url,
enabled: item.enabled,
events: item.events.split(','),
types: item.types.split(','),
};
return model;
});
return { data: { items, total: data.length } };
if (error || !data) {
return { error };
}
return { error };
const items = data.items.map((item) => {
const model: UmbWebhookDetailModel = {
entityType: UMB_WEBHOOK_ENTITY_TYPE,
unique: item.id,
url: item.url,
enabled: item.enabled,
headers: item.headers,
events: item.events,
contentTypes: item.contentTypeKeys,
};
return model;
});
return { data: { items, total: data.items.length } };
}
}

Some files were not shown because too many files have changed in this diff Show More