Update MegaMenu Application Customizer: Version bump to 1.1.0, localization support added, and unnecessary files removed

- Deleted obsolete localization file for English (en-us).
- Updated version number from 1.0.4 to 1.1.0 in manifests.js and manifests.json.
- Added localization support for German (de-de) in a new localization file.
- Removed unnecessary dependencies from manifests.
- Added .gitignore to exclude build artifacts and temporary files.
This commit is contained in:
Torsten Brendgen
2026-07-16 22:43:31 +02:00
parent a2b8165262
commit 97d2b6b5d2
53 changed files with 1071 additions and 1065 deletions

View File

@@ -4,4 +4,5 @@ export interface ISPTermStorePickerServiceProps {
hideDeprecatedTags: boolean;
hideTagsNotAvailableForTagging: boolean;
anchorId: string;
}
cacheMinutes: number;
}

View File

@@ -1,4 +1,5 @@
export function debugLog(enabled: boolean, source: string, message: string, ...args: any[]): void {
// tslint:disable:no-any
export function debugLog(enabled: boolean, source: string, message: string, ...args: any[]): void {
if (!enabled) {
return;
}
@@ -23,4 +24,4 @@ export function debugError(enabled: boolean, source: string, message: string, ..
const output: any[] = ['[' + source + '] ' + message].concat(args || []);
console.error.apply(console, output);
}
}

View File

@@ -12,6 +12,18 @@ import { ApplicationCustomizerContext } from '@microsoft/sp-application-base';
import { debugError } from './MegaMenuDebug';
const EmptyGuid: string = '00000000-0000-0000-0000-000000000000';
const TermCachePrefix: string = 'MegaMenu:TermSet:';
const PickerCachePrefix: string = 'MegaMenu:PickerTerms:';
interface ITermCacheEntry {
expiresAt: number;
value: ITermSet;
}
interface IPickerTermCacheEntry {
expiresAt: number;
value: IPickerTerm[];
}
/**
* Service implementation to manage term stores in SharePoint
@@ -31,7 +43,7 @@ export default class SPTermStorePickerService {
public async getTermLabels(termId: string): Promise<string[]> {
let result: string[] = null;
try {
const data = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="8" ObjectPathId="7" /><ObjectIdentityQuery Id="9" ObjectPathId="7" /><ObjectPath Id="11" ObjectPathId="10" /><ObjectIdentityQuery Id="12" ObjectPathId="10" /><ObjectPath Id="14" ObjectPathId="13" /><ObjectIdentityQuery Id="15" ObjectPathId="13" /><Query Id="16" ObjectPathId="13"><Query SelectAllProperties="false"><Properties><Property Name="Labels" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="7" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="10" ParentId="7" Name="GetDefaultKeywordsTermStore" /><Method Id="13" ParentId="10" Name="GetTerm"><Parameters><Parameter Type="Guid">${termId}</Parameter></Parameters></Method></ObjectPaths></Request>`;
const data = `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="8" ObjectPathId="7" /><ObjectIdentityQuery Id="9" ObjectPathId="7" /><ObjectPath Id="11" ObjectPathId="10" /><ObjectIdentityQuery Id="12" ObjectPathId="10" /><ObjectPath Id="14" ObjectPathId="13" /><ObjectIdentityQuery Id="15" ObjectPathId="13" /><Query Id="16" ObjectPathId="13"><Query SelectAllProperties="false"><Properties><Property Name="Labels" SelectAll="true"><Query SelectAllProperties="false"><Properties /></Query></Property></Properties></Query></Query></Actions><ObjectPaths><StaticMethod Id="7" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="10" ParentId="7" Name="GetDefaultSiteCollectionTermStore" /><Method Id="13" ParentId="10" Name="GetTerm"><Parameters><Parameter Type="Guid">${termId}</Parameter></Parameters></Method></ObjectPaths></Request>`;
const reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json');
@@ -43,6 +55,7 @@ export default class SPTermStorePickerService {
};
const callResult = await this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions);
this.ensureSuccessfulResponse(callResult, 'Term labels');
const jsonResult = await callResult.json();
const node = jsonResult.find(x => x._ObjectType_ === 'SP.Taxonomy.Term');
@@ -73,6 +86,7 @@ export default class SPTermStorePickerService {
};
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
this.ensureSuccessfulResponse(serviceResponse, 'Term stores');
return serviceResponse.json().then((serviceJSONResponse: any) => {
// Construct results
const termStoreResult: ITermStore[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermStore');
@@ -124,7 +138,13 @@ export default class SPTermStorePickerService {
* Retrieve all terms for the given term set
* @param termset
*/
public async getAllTerms(termset: string, hideDeprecatedTags?: boolean, hideTagsNotAvailableForTagging?: boolean, useSessionStorage: boolean = true): Promise<ITermSet> {
public async getAllTerms(
termset: string,
hideDeprecatedTags?: boolean,
hideTagsNotAvailableForTagging?: boolean,
useSessionStorage: boolean = true,
cacheMinutes: number = 15
): Promise<ITermSet> {
let termsetId: string = termset;
// Check if the provided term set property is a GUID or string
if (!this.isGuid(termset)) {
@@ -139,14 +159,14 @@ export default class SPTermStorePickerService {
}
}
const childTerms = this.getTermsById(termsetId, useSessionStorage);
const childTerms = this.getCachedTermSet(termsetId, useSessionStorage);
if (childTerms) {
return childTerms;
}
// Request body to retrieve all terms for the given term set
const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultKeywordsTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`;
const data = `<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library"><Actions><ObjectPath Id="1" ObjectPathId="0" /><ObjectIdentityQuery Id="2" ObjectPathId="0" /><ObjectPath Id="4" ObjectPathId="3" /><ObjectIdentityQuery Id="5" ObjectPathId="3" /><ObjectPath Id="7" ObjectPathId="6" /><ObjectIdentityQuery Id="8" ObjectPathId="6" /><ObjectPath Id="10" ObjectPathId="9" /><Query Id="11" ObjectPathId="6"><Query SelectAllProperties="true"><Properties /></Query></Query><Query Id="12" ObjectPathId="9"><Query SelectAllProperties="false"><Properties /></Query><ChildItemQuery SelectAllProperties="false"><Properties><Property Name="IsRoot" SelectAll="true" /><Property Name="Labels" SelectAll="true" /><Property Name="TermsCount" SelectAll="true" /><Property Name="CustomSortOrder" SelectAll="true" /><Property Name="Id" SelectAll="true" /><Property Name="Name" SelectAll="true" /><Property Name="PathOfTerm" SelectAll="true" /><Property Name="Parent" SelectAll="true" /><Property Name="LocalCustomProperties" SelectAll="true" /><Property Name="IsDeprecated" ScalarProperty="true" /><Property Name="IsAvailableForTagging" ScalarProperty="true" /></Properties></ChildItemQuery></Query></Actions><ObjectPaths><StaticMethod Id="0" Name="GetTaxonomySession" TypeId="{981cbc68-9edc-4f8d-872f-71146fcbb84f}" /><Method Id="3" ParentId="0" Name="GetDefaultSiteCollectionTermStore" /><Method Id="6" ParentId="3" Name="GetTermSet"><Parameters><Parameter Type="Guid">${termsetId}</Parameter></Parameters></Method><Method Id="9" ParentId="6" Name="GetAllTerms" /></ObjectPaths></Request>`;
const reqHeaders = new Headers();
reqHeaders.append('accept', 'application/json');
@@ -158,7 +178,11 @@ export default class SPTermStorePickerService {
};
return this.context.spHttpClient.post(this.clientServiceUrl, SPHttpClient.configurations.v1, httpPostOptions).then((serviceResponse: SPHttpClientResponse) => {
this.ensureSuccessfulResponse(serviceResponse, 'Terms');
return serviceResponse.json().then((serviceJSONResponse: any) => {
if (!Array.isArray(serviceJSONResponse)) {
throw new Error('The taxonomy service returned an unexpected response.');
}
const termStoreResultTermSets: ITermSet[] = serviceJSONResponse.filter((r: { [x: string]: string; }) => r['_ObjectType_'] === 'SP.Taxonomy.TermSet');
if (termStoreResultTermSets.length > 0) {
@@ -203,7 +227,12 @@ export default class SPTermStorePickerService {
try {
if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(termsetId, JSON.stringify(termStoreResultTermSet));
const ttlMinutes: number = this.normalizeCacheMinutes(cacheMinutes);
const cacheEntry: ITermCacheEntry = {
expiresAt: Date.now() + ttlMinutes * 60 * 1000,
value: termStoreResultTermSet
};
window.sessionStorage.setItem(TermCachePrefix + termsetId, JSON.stringify(cacheEntry));
}
} catch (error) {
// Do nothing, sometimes "storage quota exceeded" error if too many items
@@ -225,7 +254,7 @@ export default class SPTermStorePickerService {
public async searchTermsByTermId(searchText: string, termId: string): Promise<IPickerTerm[]> {
const { useSessionStorage } = this.props;
const childTerms = this.getTermsById(termId, useSessionStorage);
const childTerms = this.getCachedPickerTerms(termId, useSessionStorage);
if (childTerms) {
return this.searchTermsBySearchText(childTerms, searchText);
} else {
@@ -257,12 +286,18 @@ export default class SPTermStorePickerService {
const returnTerms: IPickerTerm[] = [];
const childTerms = this.getTermsById(anchorId, useSessionStorage);
const childTerms = this.getCachedPickerTerms(anchorId, useSessionStorage);
if (childTerms) {
return childTerms;
}
const termSet = await this.getAllTerms(termsetNameOrID, hideDeprecatedTags, hideTagsNotAvailableForTagging);
const termSet = await this.getAllTerms(
termsetNameOrID,
hideDeprecatedTags,
hideTagsNotAvailableForTagging,
useSessionStorage,
this.props.cacheMinutes
);
const terms = termSet.Terms;
if (anchorId) {
const anchorTerm = terms.filter(t => t.Id.toLowerCase() === anchorId.toLowerCase()).shift();
@@ -277,7 +312,11 @@ export default class SPTermStorePickerService {
try {
if (useSessionStorage && window.sessionStorage) {
window.sessionStorage.setItem(anchorId, JSON.stringify(returnTerms));
const cacheEntry: IPickerTermCacheEntry = {
expiresAt: Date.now() + this.normalizeCacheMinutes(this.props.cacheMinutes) * 60 * 1000,
value: returnTerms
};
window.sessionStorage.setItem(PickerCachePrefix + anchorId, JSON.stringify(cacheEntry));
}
} catch (error) {
// Do nothing
@@ -332,15 +371,21 @@ export default class SPTermStorePickerService {
return null;
}
private getTermsById(termId, useSessionStorage: boolean = true) {
private getCachedTermSet(termId: string, useSessionStorage: boolean = true): ITermSet {
try {
if (useSessionStorage && window.sessionStorage) {
const terms = window.sessionStorage.getItem(termId);
if (terms) {
return JSON.parse(terms);
} else {
const key: string = TermCachePrefix + termId;
const terms: string = window.sessionStorage.getItem(key);
if (!terms) {
return null;
}
const cacheEntry: ITermCacheEntry = JSON.parse(terms) as ITermCacheEntry;
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} else {
return null;
}
@@ -349,6 +394,42 @@ export default class SPTermStorePickerService {
}
}
private getCachedPickerTerms(termId: string, useSessionStorage: boolean = true): IPickerTerm[] {
try {
if (!useSessionStorage || !window.sessionStorage) {
return null;
}
const key: string = PickerCachePrefix + termId;
const serialized: string = window.sessionStorage.getItem(key);
if (!serialized) {
return null;
}
const cacheEntry: IPickerTermCacheEntry = JSON.parse(serialized) as IPickerTermCacheEntry;
if (!cacheEntry || !cacheEntry.value || !cacheEntry.expiresAt || cacheEntry.expiresAt <= Date.now()) {
window.sessionStorage.removeItem(key);
return null;
}
return cacheEntry.value;
} catch (error) {
return null;
}
}
private normalizeCacheMinutes(value: number): number {
if (typeof value !== 'number' || !isFinite(value)) {
return 15;
}
return Math.min(Math.max(Math.floor(value), 1), 1440);
}
private ensureSuccessfulResponse(response: SPHttpClientResponse, operation: string): void {
if (!response.ok) {
throw new Error(operation + ' request failed (' + response.status + ' ' + response.statusText + ').');
}
}
private searchTermsBySearchText(terms, searchText) {
if (terms) {
return terms.filter((t) => { return t.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1; });

View File

@@ -12,29 +12,11 @@ const LOG_SOURCE: string = 'TaxonomyNavigationService';
export class TaxonomyNavigationService implements ITaxonomyNavigationService {
private _taxonomyPickerService: SPTermStorePickerService;
private _noTerm: ITerm = {
_ObjectType_: '',
_ObjectIdentity_: '',
CustomSortOrderIndex: 0,
Description: '',
Id: '',
IsAvailableForTagging: false,
IsDeprecated: false,
IsRoot: true,
LocalCustomProperties: {
_Sys_Nav_HoverText: 'Es wurden keine Terms gefunden. Bitte ueberpruefen Sie Ihre Einstellungen.',
_Sys_Nav_ExcludedProviders: undefined,
_Sys_Nav_SimpleLinkUrl: undefined
},
Name: 'Es wurden keine Terms gefunden. Bitte ueberpruefen Sie Ihre Einstellungen.',
PathOfTerm: '',
TermSet: undefined
};
constructor(
private context: ApplicationCustomizerContext,
private termSetName: string,
private debug: boolean = false
private termSetNameOrId: string,
private debug: boolean = false,
private cacheMinutes: number = 15
) {
sp.setup({
spfxContext: context
@@ -42,10 +24,11 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
this._taxonomyPickerService = new SPTermStorePickerService(
{
anchorId: '',
termsetNameOrID: termSetName,
termsetNameOrID: termSetNameOrId,
useSessionStorage: true,
hideDeprecatedTags: true,
hideTagsNotAvailableForTagging: false
hideTagsNotAvailableForTagging: false,
cacheMinutes: cacheMinutes
},
this.context,
this.debug
@@ -54,13 +37,19 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
public async getMenuItems(): Promise<IMenuItem[]> {
const siteCollectionUrl: string = this.context.pageContext.site.absoluteUrl;
const termset: ITermSet = await this._taxonomyPickerService.getAllTerms(this.termSetName);
const termset: ITermSet = await this._taxonomyPickerService.getAllTerms(
this.termSetNameOrId,
true,
false,
true,
this.cacheMinutes
);
const itemsDict: ItemDictionary<IMenuItem> = new ItemDictionary<IMenuItem>();
const menuItems: IMenuItem[] = [];
if (!termset || !termset.Terms) {
if (!termset || !termset.Terms || termset.Terms.length === 0) {
debugWarn(this.debug, LOG_SOURCE, 'No terms found in the term set.');
return [new MenuItem(this._noTerm, 0, siteCollectionUrl)];
throw new Error('The configured navigation term set was not found or contains no terms.');
}
termset.Terms.forEach((term: ITerm) => {
@@ -80,4 +69,4 @@ export class TaxonomyNavigationService implements ITaxonomyNavigationService {
return menuItems;
}
}
}