Customize Kinetic home page?

We’re upgrading to 2025.2. Is it possible to customize the home page, ie make code changes to it? Not just the Edit / Widget options. I’m not seeing a way to do it in Application Studio.

Specifically I’m looking to make the menu/favorites/recent panel docked by default instead of auto-hide.

Thanks!

3 Likes

Due to the way that panels slide out, Kinetic has not built the homepage in a way that allows the slideout to stay popped out. It will always cover items as a slideout. To fix this, they would need to make these proper frames or something that would allow the rest of the content to be resized into the remaining area while the sidebar is popped out.

I hope I am wrong! Good luck!

2 Likes

Is there a way to customize the home page? Application studio or something else maybe?

I see I could edit index.html, make it run a little script if on the home page, onload… :smiling_face_with_horns:

Not outside of editing the layouts. However, since we are in a browser, you may be able to use some CSS edits to change some of the look and feel. For example, I made a change to the stupid tiny scroll bars using an Edge plugin. See: Look at the size of those scroll bars (Kinetic 2024.1) :SafeHarbor: - Kinetic ERP - Epicor User Help Forum

3 Likes

Ok so you’re modifying the html/css on the client side, needing a browser add-in. And you’re also saying there is no way to officially customize the home page (except for the Edit Layout options). Thanks for confirming my thoughts.

I guess I’ll be writing some javascript before we go live with upgrade…!

2 Likes

Ok well I’m pretty happy with this. This is for on-prem. Script goes in the end of index.html. Keep a backup…!
(C:\inetpub\wwwroot\YourAppPool\Server\Apps\ERP\Home\index.html)

<script>
//custom code to auto-open kinetic menu. Pin it open on smart client. Only on home page.
function waitForMenuThenClick() {
    const pollId = setInterval(() => {
        var menuElement = document.querySelector('li#Menu');
        if (menuElement) {
			//Menu exists, but is still loading. Wait before opening menu.
            clearInterval(pollId); 
            setTimeout(() => {
				//Only open menu automatically on Home page.
				if(document.title=='Home'){
					menuElement.click();
					
					//If using the smart client, pin the menu.  Not in the browser as it likes to stay in the same tab.
					const isSmartClient = (window.outerHeight - window.innerHeight == 0); //smart client has no address bar etc.
                    if (isSmartClient) {
						var thumbtackElement = document.querySelector('i.ep-pinned-allow');
						thumbtackElement.click();
					}
				}
            }, 500);
        }
    }, 100);
}
waitForMenuThenClick();
</script>

Thanks Nate for steering me in this direction.

6 Likes

That’s pretty nifty, sir!

I wonder if it could be injected into the homepage on SaaS via the get homepage tiles BO Method.. hmm..

5 Likes

Turns out you can do it in an embedded web app or webisite widget.
But only if the URL is on the same server. So still not sure how the on-prem folks can do this…
As for injecting it, I looked into Ice.BO.ShellLayout.GetHomePageForUser
It has a HomePageTableset
But I don’t see an obvious way to inject html code.

For widget (on app server):

<!DOCTYPE html>
<html>
<head>
<script>
//custom code to open Home Page menu after page load. Pin it open on smart client.  Don't do anything on other pages.
function waitForMenuThenClick() {
	let timer = 0;
    const pollId = setInterval(() => {
        let menuElement = parent.document.querySelector('li#Menu');
        if (menuElement) {
			//Menu exists, but is probably still loading.  Wait a bit before opening menu.
            clearInterval(pollId); 
            setTimeout(() => {
				//Only open menu automatically on Home page.
				if(parent.document.title=='Home'){
					menuElement.click();
					
					//If using the smart client, pin the menu.  Don't pin the menu in the browser as it likes to stay in the same tab.
					const isSmartClient = (parent.outerHeight - parent.innerHeight == 0); //smart client has no address bar etc.
                    if (isSmartClient) {
						setTimeout(() => {
							const thumbtackElement = parent.document.querySelector('i.ep-pinned-allow');
							if (thumbtackElement) {
								thumbtackElement.click();
							}
						}, 500);
					}
				}
            }, 500);  //pause before clicking
        } else {
			timer += 100;
			if (timer > 10000){
				clearInterval(pollId); 
			}
		}
    }, 100);  //Look for menu element every 100ms
}
waitForMenuThenClick();
</script>
</head>
<body>
Menu-Auto-Open Widget
</body>
</html>
3 Likes

Good show!
I think we can probably combine this, with this, i’m tinkering with it a bit now.:

https://www.epiusers.help/t/dev-tools-developers-swiss-army-knife-server-logging-file-download-efxeditor-appstudiojsoneditor-baqaliaseditor-reporteditor-ssrsdesignpreview-bpm-linq-on-the-fly-kinetic-dashboard/127887/121?u=gabefranco

5 Likes

This BPM on Ice.BO.ShellLayout.GetHomePageForUser (Post) injects your HTML directly as base64 encoded data and loads it as an iframe src into the second tile of a new home page tab group that is injected into whatever home page the user has set/configured. (so it will load no matter what the user’s got as a set up homepage)

This can also be saved into Db.ShellLayout so that it can load without needing the bpm, but i loaded it this way for easy testing, i just have to change the object in C# to adjust the home page.

This works successfully to load your designed html page into an iframe, but the javascript inside doesn’t seem to take effect on the parent homepage.

The javascript probably has to be tweaked to affect the parent page from here, or something or other. Or maybe browser security won’t let it because it’s loading from a [null] context vs. the url the rest of kinetic is loading from. I’ll play around more tomorrow.

//using Newtonsoft.Json;
//using Newtonsoft.Json.Linq;

var webPropsGroup = new JObject {
    ["hidden"] = false,
    ["x"] = 0,
    ["y"] = 0,
    ["width"] = 10,
    ["height"] = 6,
    ["kinetic"] = true,
    ["sequence"] = 0
};

var webPropsTile = new JObject
{
    ["kinetic"] = true,
    ["xx"] = 0,
    ["yy"] = 0,
    ["width"] = 2,
    ["height"] = 2
};

var newGroup = new HomeTileGroupRow {
    GroupID = 1,
    Title = "Epicor",
    Sequence = 1,
    IsFaveDefault = false,
    Type = "L",
    Retain = false,
    SysRowID = Guid.NewGuid(),
    RowMod = "A",
    WebProperties = webPropsGroup.ToString()
};


var json = JsonConvert.SerializeObject(result.HomeTileGroup);
var arr = JArray.Parse(json);
var newJRow = new JObject {
    ["GroupID"] = newGroup.GroupID,
    ["Title"] = newGroup.Title,
    ["Sequence"] = newGroup.Sequence,
    ["IsFaveDefault"] = newGroup.IsFaveDefault,
    ["Type"] = newGroup.Type,
    ["Retain"] = newGroup.Retain,
    ["SysRowID"] = newGroup.SysRowID.ToString(),
    ["RowMod"] = newGroup.RowMod,
    ["WebProperties"] = newGroup.WebProperties
};
arr.Insert(0, newJRow);


result.HomeTileGroup.Clear();
foreach(var item in arr) {
    result.HomeTileGroup.Add(JsonConvert.DeserializeObject<HomeTileGroupRow>(item.ToString()));
}

var devTile = new HomeTileRow {
    TileID = 1001,
    GroupID = newGroup.GroupID,
    Sequence = 10,
    Type = "K",
    LinkType = "F",
    Color = "FaveColor5",
    DefaultWidth = 2,
    DefaultHeight = 2,
    MaxWidth = 2,
    MaxHeight = 2,
    ExpandedFlag = false,
    Title = "Dev Tools",
    Path = "UDDVTOOL",
    DisplayType = "D",
    DisplayPath = "",
    RefreshInterval = 0,
    Company = "",
    Appserver = "",
    Plant = "",
    WebProperties = webPropsTile.ToString(),
    OpenInNewTab = true,
    ImageBlob = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="),
    RowMod = "A"
};

var embedHTMLTile = new HomeTileRow {
    TileID = 1002,
    GroupID = newGroup.GroupID,
    Type = "K",
    Path = "data:text/html;base64,PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHNjcmlwdD5mdW5jdGlvbiB3YWl0Rm9yTWVudVRoZW5DbGljaygpeyBsZXQgdGltZXIgPSAwOyBjb25zdCBwb2xsSWQgPSBzZXRJbnRlcnZhbCgoKSA9PiB7IGxldCBtZW51RWxlbWVudCA9IHBhcmVudC5kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdsaSNNZW51Jyk7IGlmIChtZW51RWxlbWVudCkgeyBjbGVhckludGVydmFsKHBvbGxJZCk7IHNldFRpbWVvdXQoKCkgPT4geyBpZihwYXJlbnQuZG9jdW1lbnQudGl0bGU9PSdIb21lJyl7IG1lbnVFbGVtZW50LmNsaWNrKCk7IGNvbnN0IGlzU21hcnRDbGllbnQgPSAocGFyZW50Lm91dGVySGVpZ2h0IC0gcGFyZW50LmlubmVySGVpZ2h0ID09IDApOyBpZiAoaXNTbWFydENsaWVudCkgeyBzZXRUaW1lb3V0KCgpID0+IHsgY29uc3QgdGh1bWJ0YWNrRWxlbWVudCA9IHBhcmVudC5kb2N1bWVudC5xdWVyeVNlbGVjdG9yKCdpLmVwLXBpbm5lZC1hbGxvdycpOyBpZiAodGh1bWJ0YWNrRWxlbWVudCkgeyB0aHVtYnRhY2tFbGVtZW50LmNsaWNrKCk7IH0gfSwgNTAwKTsgfSB9IH0sIDUwMCk7IH0gZWxzZSB7IHRpbWVyICs9IDEwMDsgaWYgKHRpbWVyID4gMTAwMDApeyBjbGVhckludGVydmFsKHBvbGxJZCk7IH0gfSB9LCAxMDApOyB9IHdhaXRGb3JNZW51VGhlbkNsaWNrKCk7PC9zY3JpcHQ+PC9oZWFkPjxib2R5Pk1lbnUtQXV0by1PcGVuIFdpZGdldDwvYm9keT48L2h0bWw+",
    LinkType = "U",
    DisplayType = "D",
    DisplayPath = "",
    LineLinkType = "",
    LinePath = "",
    BaqId = "",
    Color = "FaveColor5",
    Title = "testingtitle",
    DefaultWidth = 1,
    DefaultHeight = 1,
    MaxWidth = 1,
    MaxHeight = 1,
    ListImage = "",
    FavoriteFolderSeq = 0,
    ExpandedFlag = false,
    BaqColumnList = "",
    Sequence = 20,
    RelatedMenuId = "",
    RefreshInterval = 0,
    Company = "",
    Appserver = "",
    BaqContextColumn = "",
    Plant = "",
    MetricAggregate = "",
    MetricTextPrefix = "",
    MetricTextSuffix = "",
    MetricImage = "",
    MetricTextFontSize = 0,
    ImageRowID = Guid.Empty,
    ImageBlob = null,
    ImageFilename = "",
    WebProperties = "{\"token\":false,\"context\":false,\"kinetic\":true,\"xx\":2,\"yy\":0,\"width\":2,\"height\":2}",
    OpenInNewTab = false,
    SysRowID = Guid.Empty,
    RowMod = "A"
};

var tileJson = JsonConvert.SerializeObject(result.HomeTile);
var tileArr = JArray.Parse(tileJson);

var devJRow = JObject.FromObject(devTile);
var embedHTMLJRow = JObject.FromObject(embedHTMLTile);
tileArr.Insert(0, embedHTMLJRow);
tileArr.Insert(0, devJRow);
result.HomeTile.Clear();
foreach (var t in tileArr)
    result.HomeTile.Add(JsonConvert.DeserializeObject<HomeTileRow>(t.ToString()));

In Dev Tools / Elements, I can see the iframe src rendering properly:

Ahh, I think it is browser security preventing it, i can see your function executing repeatedly on it’s timer, but not finding the menu:

image

Maybe we could inject a listener into the parent homepage that the child iframe can talk to. (but then I guess, if we can inject into parent homepage, we just put the script there instead :sweat_smile:)… Hmmm…

Validated, if I launch chrome with web security disabled, it functions just fine.

& "C:\Program Files\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir="C:/ChromeDev"

Hmmm.. I’m thinking what if we upload the html somewhere sanctioned by Sandbox.IO and accessible via a direct url link.. like into the Company folder and then we craft a URL that is a REST or odata call to pull that file into the iframe… Ice.Lib.FileTransferSvc doesn’t have a GET version of DownloadFile tho.. … hmmm

4 Likes

Success! Without needing the security kludge.

The below BPM on Ice.BO.ShellLayout.GetHomePageForUser (Post) injects your javascript directly into the homepage via an iframe src. The logic can be made more robust so that instead of injecting a tab, it detects the user’s first tab/page and append the tile after all existing tiles by walking through the x/y coordinates of existing tiles to arrive at a good “empty space” to host the tile dynamically.

I have some work complete on a model that works similarly.

//using Newtonsoft.Json;
//using Newtonsoft.Json.Linq;
var webPropsGroup = new JObject { ["hidden"] = false, ["x"] = 0, ["y"] = 0, ["width"] = 10, ["height"] = 6, ["kinetic"] = true, ["sequence"] = 0 };
var webPropsTile = new JObject { ["kinetic"] = true, ["xx"] = 0, ["yy"] = 0, ["width"] = 2, ["height"] = 2 };
var webPropsEmbedHTMLTile = new JObject { ["hidden"] = true, ["context"] = false, ["token"] = false, ["kinetic"] = true, ["xx"] = 2, ["yy"] = 0, ["width"] = 2, ["height"] = 2 };

var newGroup = new HomeTileGroupRow {
    GroupID = 1, Title = "Epicor", Sequence = 1, IsFaveDefault = false, Type = "L", Retain = false, 
    SysRowID = Guid.NewGuid(), RowMod = "A", WebProperties = webPropsGroup.ToString()
};

var groupList = result.HomeTileGroup.ToList();
groupList.Insert(0, newGroup);
result.HomeTileGroup.Clear();
groupList.ForEach(result.HomeTileGroup.Add);

var devTile = new HomeTileRow {
    TileID = 1001, GroupID = newGroup.GroupID, Sequence = 10, Type = "K", LinkType = "F", Color = "FaveColor5", 
    DefaultWidth = 2, DefaultHeight = 2, MaxWidth = 2, MaxHeight = 2, ExpandedFlag = false, Title = "Dev Tools", 
    Path = "UDDVTOOL", DisplayType = "D", WebProperties = webPropsTile.ToString(), OpenInNewTab = true, 
    ImageBlob = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="), RowMod = "A"
};

var embedHTMLTile = new HomeTileRow {
    TileID = 1002, GroupID = newGroup.GroupID, Type = "K", LinkType = "U", DisplayType = "D", Color = "FaveColor5", 
    Title = "There be dragons here", DefaultWidth = 1, DefaultHeight = 1, MaxWidth = 1, MaxHeight = 1, Sequence = 20, 
    WebProperties = webPropsEmbedHTMLTile.ToString(), RowMod = "A", Path = "javascript:(function()%7Blet%20timer%20%3D%200%3Bconst%20pollId%20%3D%20setInterval(()%20%3D%3E%20%7Blet%20menuElement%20%3D%20parent.document.querySelector('li%23Menu')%3Bif%20(menuElement)%20%7BclearInterval(pollId)%3BsetTimeout(()%20%3D%3E%20%7Bif(parent.document.title%3D%3D'Home')%7BmenuElement.click()%3Bconst%20isSmartClient%20%3D%20(parent.outerHeight%20-%20parent.innerHeight%20%3D%3D%200)%3Bif%20(isSmartClient)%20%7BsetTimeout(()%20%3D%3E%20%7Bconst%20thumbtackElement%20%3D%20parent.document.querySelector('i.ep-pinned-allow')%3Bif%20(thumbtackElement)%20%7BthumbtackElement.click()%3B%7D%7D%2C%20500)%3B%7D%7D%7D%2C%20500)%3B%7D%20else%20%7Btimer%20%2B%3D%20100%3Bif%20(timer%20%3E%2010000)%7BclearInterval(pollId)%3B%7D%7D%7D%2C%20100)%3B%7D)()%3B"
};

var tileList = result.HomeTile.ToList();
tileList.InsertRange(0, new[] { devTile, embedHTMLTile });
result.HomeTile.Clear();
tileList.ForEach(result.HomeTile.Add);

You can try to save this to a normal homepage tile like this:

javascript:(function()%7Blet%20timer%20%3D%200%3Bconst%20pollId%20%3D%20setInterval(()%20%3D%3E%20%7Blet%20menuElement%20%3D%20parent.document.querySelector('li%23Menu')%3Bif%20(menuElement)%20%7BclearInterval(pollId)%3BsetTimeout(()%20%3D%3E%20%7Bif(parent.document.title%3D%3D'Home')%7BmenuElement.click()%3Bconst%20isSmartClient%20%3D%20(parent.outerHeight%20-%20parent.innerHeight%20%3D%3D%200)%3Bif%20(isSmartClient)%20%7BsetTimeout(()%20%3D%3E%20%7Bconst%20thumbtackElement%20%3D%20parent.document.querySelector('i.ep-pinned-allow')%3Bif%20(thumbtackElement)%20%7BthumbtackElement.click()%3B%7D%7D%2C%20500)%3B%7D%7D%7D%2C%20500)%3B%7D%20else%20%7Btimer%20%2B%3D%20100%3Bif%20(timer%20%3E%2010000)%7BclearInterval(pollId)%3B%7D%7D%7D%2C%20100)%3B%7D)()%3B

But, Epicor is helpful and it prepends https:// to it since it doesn’t detect it in the URL.
You can use Postman or similar to capture the payload, manually edit to remove the https:// part, then it should commit to Db without that, from there you can save it as default homepage, and now it will apply for everyone without the BPM.

I’m not sure yet where ICE.BO.ShellLayout/UpdateHomePageForUser saves the HomeTileRow in Db to overlay (maybe it is with the rest of user personalizations), but also you can just edit it directly in a function instead of the REST call (when we find it)

Also you can manually edit Db.ShellLayout row that is default for your company to manually add the tile.

Edit: @TerryR: Any chance you could make a version of your fancy javascript thingymajig do one more click to expand the Main Menu ->?

4 Likes

Is this to say the website component allows javascript: URLs or raw data: URLs AND the js works on the parent DOM without browser security disabled? Should be impossible. Unless they are serving ‘unsafe-inline’ CSP header on the parent page which is unlikely (edit: apparently more likely than i thought) and brittle in modern browsers.

1 Like

It doesn’t end up being parent child because the JavaScript is injected directly to the home page vs. loading an iframe into another page, even though we are using the embedded URL widget, I think. So it ends up being handled with the same security scope as the home page. Or maybe not! I really don’t know much about browser security, but it works :nerd_face:

2 Likes

I can have my doggie with me across all of Epicor now, yay. Instantly improved the product.


javascript:(function(){const id='dog-overlay-unique-id';if(parent.document.getElementById(id))return;const d=parent.document.createElement('div');d.id=id;Object.assign(d.style,{position:'fixed',bottom:'20px',right:'20px',fontSize:'50px',opacity:'0.8',pointerEvents:'none',zIndex:'9999'});d.innerText='🐶';parent.document.body.appendChild(d);})();
3 Likes

You can test any website, including your internal web apps, here:

https://securityheaders.com/

2 Likes

Wow you did it, that is super cool. I don’t think browser security should be tripping it up if the script comes from the same server…

Good idea to auto expand the menu if it loads closed. Here is updated script…

<script>
//custom code to open Home Page menu after page load. Pin it open on smart client.  Don't do anything on other pages.

function waitForMenuThenClick() {

    //allow same code to run whether this code is in the main page or an iframe.
	const mainPage = (window.self !== window.top) ? parent.document : document;
	const browserWindow = (window.self !== window.top) ? window.parent : window;
	let menuClicked = false;

    const observer = new MutationObserver((mutations, obs) => {
	
        //Only open menu automatically on Home page.
        if (mainPage.title !== 'Home') {
            obs.disconnect();
            return;
        }

        //Open menu if it isn't already open
		const menuElement = mainPage.querySelector('li#Menu');
        if (menuElement && !menuElement.classList.contains('active') && !menuClicked) {
			menuClicked = true; //avoid infinite loop
			//alert (mainPage.title);
			menuElement.click();
        }

        //Wait for menu to open, look for first entry ("Main Menu" by default)
        const firstMenuItem = mainPage.querySelector('.k-treeview-toggle');  //the little arrow next to "Main Menu"
        if (firstMenuItem) {
		
			//If using the smart client, pin the menu.  Don't pin the menu in the browser as it likes to open menu links in the same tab.
            const isSmartClient = (browserWindow.outerHeight - browserWindow.innerHeight === 0); //smart client has no address bar etc.
            if (isSmartClient) {
                const thumbtackElement = mainPage.querySelector('i.ep-pinned-allow');
                if (thumbtackElement) {
                    thumbtackElement.click();
                }
            }

			//Wait a moment then expand the main menu automatically if it is collapsed
            setTimeout(() => {
			    const menuIsExpanded = firstMenuItem.parentElement.parentElement.getAttribute('aria-expanded');  //false if collapsed
				if (menuIsExpanded === "false") {
					firstMenuItem.click()
				}
            },100);

			obs.disconnect();
        }
    });

    // Start observing
	observer.observe(mainPage.body, {
		childList: true,
		subtree: true,
		attributes: true //catch attribute changes like 'aria-expanded'
	});

    // Safety timeout to stop watching after 10 seconds
    setTimeout(() => observer.disconnect(), 10000);
}
setTimeout(() => waitForMenuThenClick(), 2000); //it takes a moment for the page name to update from Home to whatever the page is.
</script>

Update - rewrote using MutationObserver instead of polling/arbitrary timeouts.

3 Likes

Alright here are the finalized hacks we’re deploying with. Maybe useful to some of you other on-prem dinosaurs…

“Fixes”:

  • Menu pops open by default.
  • Page titles not unnecessarily truncated
  • “Where is January!??!” emails stopped
  • Icons and program name match the product.

Bonuses, added after initial post…

  • On grids/dashboards, replace “No records available” with “Loading…” when the page is loading…
  • Make loading bar fatter, animate.

Code (add this to the end of your index.html):

<script>
//custom code to open Home Page menu after page load. Pin it open on smart client.  Don't do anything on other pages.

function waitForMenuThenClick() {

    //allow same code to run whether this code is in the main page or an iframe.
	const mainPage = (window.self !== window.top) ? parent.document : document;
	const browserWindow = (window.self !== window.top) ? window.parent : window;
	let menuClicked = false;

    const observer = new MutationObserver((mutations, obs) => {
		//Wait for loading to stop
		const loadingBar = mainPage.querySelector('.ep-loading-progress');
        if (!loadingBar.classList.contains('ep-progress-bar-off')) {
            return;  //page still loading, don't do anything yet
        }
		
		//Check page title and if menubar is present
		const pageTitleH1 = mainPage.getElementById('ep-view-title');
		const menuElement = mainPage.querySelector('li#Menu');
		if (!pageTitleH1 || !menuElement) return;
		if (!pageTitleH1.title === '') return;

		//Page is loaded now so we can stop listening for future changes.  But keep running below.
		obs.disconnect();

		//Only open menu on home page
		if (pageTitleH1.title !== 'Home'){
			return;
		}

        //Make sure menu isn't already open, open if not
        if (!menuElement.classList.contains('active') && !menuClicked) {
			menuClicked = true; //avoid infinite loop
			menuElement.click();
        }

        //Wait a moment for menu to open, look for first entry ("Main Menu" by default)
		const firstMenuItem = mainPage.querySelector('.k-treeview-toggle');  //the little arrow next to "Main Menu"
		setTimeout(() => {
			if (firstMenuItem) {
			
				//Option to only pin the menu in the smart client, as the browser defaults to opening items in the same tab.  Smart client opens links in a separate window.
				//const isSmartClient = ((browserWindow.outerHeight - browserWindow.innerHeight) == 0); //smart client has no address bar etc.
				//if (isSmartClient) {
				const thumbtackElement = mainPage.querySelector('i.ep-pinned-allow');
				if (thumbtackElement) {
					thumbtackElement.click();
				}
				//}

				//Also expand the main menu automatically if it is collapsed
				const menuIsExpanded = firstMenuItem.parentElement.parentElement.getAttribute('aria-expanded');  //false if collapsed
				if (menuIsExpanded === "false") {
					firstMenuItem.click()
				}
			}
		},100);
    });

    // Start observing
	observer.observe(mainPage.body, {
		attributes: true,    // Watch for changes to 'title' or other attributes
		childList: true,     // Watch for changes to the text inside the tag
		subtree: true       // Only watch the element itself, not deep nested children
	});

    // Safety timeout to stop watching after 10 seconds
    setTimeout(() => {
		observer.disconnect()
	}, 30000);
}
waitForMenuThenClick();

</script>

<style>
	/* fix overly truncated page titles */
    .ep-view-header div.ep-view-title {
		width: 100% !important;
		max-width:100% !important;
        padding-right: 250px !important;
        box-sizing: border-box !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
		user-select: auto !important;
		z-index: 1;
		position: relative;
    }
    div.ep-view-activity {
		z-index:2 !important;
	}
	
	/* fix calendar popups to show Januray instead of just the year. */
	span.k-calendar-navigation-marker::before {
        content: "Jan " !important;
    }

	/* On grids and dashboards, replace "No Records Available" with "Loading..." when the page is loading... */
	body:has(.ep-loading-progress:not(.ep-progress-bar-off)) p.ep-grid-no-records {
		visibility: hidden;
		position: relative;
	}
	body:has(.ep-loading-progress:not(.ep-progress-bar-off)) p.ep-grid-no-records::before {
		content: "Loading...";
		visibility: visible;
	}

	/* make loading progress bar fatter, animate */
	.ep-loading-progress span { 
		height: 6px !important;
		animation: ep-anim-show-progress 0.5s linear infinite !important;
	}
	@keyframes ep-anim-show-progress {
		from {
			background-position: 0 0;
		}
		to {
			background-position: 15px 0; /* Match background-size width for a seamless loop */
		}
	}

</style>

The home menu thing will work inside a widget like how @GabeFranco figured out, though the other two fixes won’t work.

Oh and FWIW ($-12 now..), here is my branding idea, prior to them axing on-prem…
Epicor Ideas: Change Logo/Icon/Banner/Shortcuts/FolderName from “epicor” to “Kinetic”

4 Likes

Where is January gets me everytime.

3 Likes

One more… (I can’t edit old post):

We found that users were accidentally clicking on the EDD icon in the nav bar, it would launch a browser in the background, then start randomly popping up login errors. So I hid the EDD icon for most users.

	/* Janky way to hide the EDD icon in menu for most users.  
If the Executive Analysis menu item is not present for the user then hide the EDD icon.
Need to find the menu element ID number for Executive Analysis (eg 0_18) */
	:root:not(:has([id*="0_18"])) li.ep-menu-bar-item#Charts {
		display: none !important;
	}

1 Like

There is a method for getting the EDD Url or similar. I have it tied to a security group. If the user is not part of the group, then the icon in the toolbar does not show. Part of the shell layout bpms IIR.

2 Likes