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:
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* This script is used to compare the keys in the localization files. It will take a main language (en.js) and compare with the other languages.
|
||||
* The script will output the keys that are missing in the other languages and the keys that are missing in the main language.
|
||||
*
|
||||
* Note: Since the source files are TypeScript files, the script will only compare on the dist-cms files.
|
||||
*
|
||||
* Usage: node devops/localization/compare-languages.js [filter]
|
||||
* Example: node devops/localization/compare-languages.js da-dk.js
|
||||
*
|
||||
* Copyright (c) 2024 by Umbraco HQ
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const mainLanguage = 'en.js';
|
||||
const __dirname = import.meta.dirname;
|
||||
const languageFolder = path.join(__dirname, '../../dist-cms/assets/lang');
|
||||
|
||||
// Check that the languageFolder exists
|
||||
if (!fs.existsSync(languageFolder)) {
|
||||
console.error(`The language folder does not exist: ${languageFolder}. You need to build the project first by running 'npm run build'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mainKeys = (await import(path.join(languageFolder, mainLanguage))).default;
|
||||
const mainMap = buildMap(mainKeys);
|
||||
|
||||
const filter = process.argv[2];
|
||||
if (filter) {
|
||||
console.log(`Filtering on: ${filter}`);
|
||||
}
|
||||
|
||||
const languages = fs.readdirSync(languageFolder).filter((file) => file !== mainLanguage && file.endsWith('.js') && (!filter || file.includes(filter)));
|
||||
const missingKeysInMain = [];
|
||||
|
||||
const languagePromise = Promise.all(languages.map(async (language) => {
|
||||
const languageKeys = (await import(path.join(languageFolder, language))).default;
|
||||
const languageMap = buildMap(languageKeys);
|
||||
|
||||
const missingKeys = Array.from(mainMap.keys()).filter((key) => !languageMap.has(key));
|
||||
let localMissingKeysInMain = Array.from(languageMap.keys()).filter((key) => !mainMap.has(key));
|
||||
localMissingKeysInMain = localMissingKeysInMain.map((key) => `${key} (${language})`);
|
||||
missingKeysInMain.push(...localMissingKeysInMain);
|
||||
|
||||
console.log(`\n${language}:`);
|
||||
console.log(`Missing keys in ${language}:`);
|
||||
console.log(missingKeys);
|
||||
}));
|
||||
|
||||
await languagePromise;
|
||||
|
||||
console.log(`Missing keys in ${mainLanguage}:`);
|
||||
console.log(missingKeysInMain);
|
||||
|
||||
function buildMap(keys) {
|
||||
const map = new Map();
|
||||
|
||||
for (const key in keys) {
|
||||
for (const subKey in keys[key]) {
|
||||
map.set(`${key}_${subKey}`, keys[key][subKey]);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* This script is used to find unused language keys in the javascript files. It will take a main language (en.js) and compare with the other languages.
|
||||
*
|
||||
* Usage: node devops/localization/unused-language-keys.js
|
||||
* Example: node devops/localization/unused-language-keys.js
|
||||
*
|
||||
* Copyright (c) 2024 by Umbraco HQ
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import glob from 'tiny-glob';
|
||||
|
||||
const mainLanguage = 'en.js';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
const languageFolder = path.join(__dirname, '../../dist-cms/assets/lang');
|
||||
|
||||
// Check that the languageFolder exists
|
||||
if (!fs.existsSync(languageFolder)) {
|
||||
console.error(`The language folder does not exist: ${languageFolder}. You need to build the project first by running 'npm run build'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mainKeys = (await import(path.join(languageFolder, mainLanguage))).default;
|
||||
const mainMap = buildMap(mainKeys);
|
||||
const keys = Array.from(mainMap.keys());
|
||||
const usedKeys = new Set();
|
||||
|
||||
const elementAndControllerFiles = await glob(`${__dirname}/../../src/**/*.ts`);
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -166,8 +166,8 @@ export default {
|
||||
confirmlogout: 'האם הינך בטוח?',
|
||||
confirmSure: 'האם אתה בטוח?',
|
||||
cut: 'גזור',
|
||||
editdictionary: 'ערוך פרט מילון',
|
||||
editlanguage: 'ערוך שפה',
|
||||
editDictionary: 'ערוך פרט מילון',
|
||||
editLanguage: 'ערוך שפה',
|
||||
insertAnchor: 'הוסף קישור מקומי',
|
||||
insertCharacter: 'הוסף תו',
|
||||
insertgraphicheadline: 'הוסף פס גרפי',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -215,8 +215,8 @@ export default {
|
||||
confirmlogout: 'ログアウトしますか?',
|
||||
confirmSure: '本当にいいですか?',
|
||||
cut: '切り取り',
|
||||
editdictionary: 'ディクショナリのアイテムの編集',
|
||||
editlanguage: '言語の編集',
|
||||
editDictionary: 'ディクショナリのアイテムの編集',
|
||||
editLanguage: '言語の編集',
|
||||
insertAnchor: 'アンカーの挿入',
|
||||
insertCharacter: '文字の挿入',
|
||||
insertgraphicheadline: 'ヘッドライン画像の挿入',
|
||||
|
||||
@@ -166,8 +166,8 @@ export default {
|
||||
confirmlogout: '로그아웃 하시겠습니까?',
|
||||
confirmSure: '확실합니까?',
|
||||
cut: "TRANSLATE ME: 'Cut'",
|
||||
editdictionary: '사전 항목 편집',
|
||||
editlanguage: '언어 편집',
|
||||
editDictionary: '사전 항목 편집',
|
||||
editLanguage: '언어 편집',
|
||||
insertAnchor: '내부 링크삽입',
|
||||
insertCharacter: '문자열 삽입',
|
||||
insertgraphicheadline: '그래픽 헤드라인 삽입',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -347,8 +347,8 @@ export default {
|
||||
confirmlogout: 'Вы уверены?',
|
||||
confirmSure: 'Вы уверены?',
|
||||
cut: 'Вырезать',
|
||||
editdictionary: 'Править статью словаря',
|
||||
editlanguage: 'Изменить язык',
|
||||
editDictionary: 'Править статью словаря',
|
||||
editLanguage: 'Изменить язык',
|
||||
insertAnchor: 'Вставить локальную ссылку (якорь)',
|
||||
insertCharacter: 'Вставить символ',
|
||||
insertgraphicheadline: 'Вставить графический заголовок',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -346,8 +346,8 @@ export default {
|
||||
confirmlogout: 'Ви впевнені?',
|
||||
confirmSure: 'Ви впевнені?',
|
||||
cut: 'Вирізати',
|
||||
editdictionary: 'Редагувати статтю словника',
|
||||
editlanguage: 'Змінити мову',
|
||||
editDictionary: 'Редагувати статтю словника',
|
||||
editLanguage: 'Змінити мову',
|
||||
insertAnchor: 'Вставити локальне посилання (якір)',
|
||||
insertCharacter: 'Вставити символ',
|
||||
insertgraphicheadline: 'Вставити графічний заголовок',
|
||||
|
||||
@@ -221,8 +221,8 @@ export default {
|
||||
confirmlogout: '您确定吗?',
|
||||
confirmSure: '您确定吗?',
|
||||
cut: '剪切',
|
||||
editdictionary: '编辑字典项',
|
||||
editlanguage: '编辑语言',
|
||||
editDictionary: '编辑字典项',
|
||||
editLanguage: '编辑语言',
|
||||
insertAnchor: '插入本地链接',
|
||||
insertCharacter: '插入字符',
|
||||
insertgraphicheadline: '插入图片标题',
|
||||
|
||||
@@ -220,8 +220,8 @@ export default {
|
||||
confirmlogout: '您確定嗎?',
|
||||
confirmSure: '您確定嗎?',
|
||||
cut: '剪切',
|
||||
editdictionary: '編輯字典項',
|
||||
editlanguage: '編輯語言',
|
||||
editDictionary: '編輯字典項',
|
||||
editLanguage: '編輯語言',
|
||||
insertAnchor: '插入本地連結',
|
||||
insertCharacter: '插入字元',
|
||||
insertgraphicheadline: '插入圖片標題',
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import './split-panel.element.js';
|
||||
|
||||
export * from './split-panel.element.js';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`,
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './repository/index.js';
|
||||
@@ -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];
|
||||
@@ -0,0 +1,2 @@
|
||||
export { UmbOEmbedRepository } from './oembed.repository.js';
|
||||
export { UMB_OEMBED_REPOSITORY_ALIAS } from './manifests.js';
|
||||
@@ -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];
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
@@ -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}">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './embedded-media/index.js';
|
||||
@@ -5,6 +5,7 @@ export interface UmbConfirmModalData {
|
||||
headline: string;
|
||||
content: TemplateResult | string;
|
||||
color?: 'positive' | 'danger';
|
||||
cancelLabel?: string;
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -29,6 +29,7 @@ export class UmbSectionSidebarElement extends UmbLitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#scroll-container {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>`)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
import './checkbox-list/components/index.js';
|
||||
import './content-picker/components/index.js';
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './modal/index.js';
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './examine-fields-settings-modal.element.js';
|
||||
export * from './examine-fields-settings-modal.token.js';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './examine-fields-viewer-modal.element.js';
|
||||
export * from './examine-fields-viewer-modal.token.js';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './fields-settings/index.js';
|
||||
export * from './fields-viewer/index.js';
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user