Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(edit-content): Reset UI state when navigating between portlets #31410

Merged
merged 6 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
643 changes: 643 additions & 0 deletions .cursor/rules/dotcms-angular-rules.mdc

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions .cursor/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"workspaceIndexing": {
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/.nx/**",
"**/*.timestamp-*",
"**/.vite/**"
]
}
}
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,3 @@ dotCMS/dependencies.gradle
.cursorrules
.cursorignore
**/vite.config.mts.timestamp-*
.cursor/
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DotCustomEventHandlerService } from '@dotcms/app/api/services/dot-custo
import { DotMenuService } from '@dotcms/app/api/services/dot-menu.service';
import { DotContentTypeService, DotIframeService, DotRouterService } from '@dotcms/data-access';
import { DotcmsEventsService, LoggerService, SiteService } from '@dotcms/dotcms-js';
import { initContentEditSessionStorage } from '@dotcms/edit-content';
import { DotLoadingIndicatorService } from '@dotcms/utils';

@Component({
Expand Down Expand Up @@ -63,6 +64,9 @@ export class IframePortletLegacyComponent implements OnInit, OnDestroy {
});

this.subscribeToAIGeneration();

// Workaroud to remove edit-content ui state
initContentEditSessionStorage();
}

ngOnDestroy(): void {
Expand Down
1 change: 1 addition & 0 deletions core-web/libs/edit-content/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/edit-content.routes';
export * from './lib/fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component';
export { initContentEditSessionStorage } from './lib/utils/functions.util';
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ export function withUI() {
/**
* Computed property that returns the active sidebar tab
*/
activeSidebarTab: computed(() => store.uiState().activeSidebarTab)
activeSidebarTab: computed(() => {
const initialState = store.initialContentletState();
const uiState = store.uiState();

return initialState === 'new' ? 0 : uiState.activeSidebarTab;
})
})),
withMethods((store) => ({
/**
Expand Down
90 changes: 90 additions & 0 deletions core-web/libs/edit-content/src/lib/utils/functions.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
generatePreviewUrl,
getFieldVariablesParsed,
getStoredUIState,
initContentEditSessionStorage,
isFilteredType,
isValidJson,
sortLocalesTranslatedFirst,
Expand All @@ -25,6 +26,7 @@ import { FLATTENED_FIELD_TYPES } from '../models/dot-edit-content-field.constant
import { DotEditContentFieldSingleSelectableDataType } from '../models/dot-edit-content-field.enum';
import { NON_FORM_CONTROL_FIELD_TYPES } from '../models/dot-edit-content-form.enum';
import { UI_STORAGE_KEY } from '../models/dot-edit-content.constant';
import { UIState } from '../models/dot-edit-content.model';

describe('Utils Functions', () => {
const { castSingleSelectableValue, getSingleSelectableFieldOptions, getFinalCastedValue } =
Expand Down Expand Up @@ -802,4 +804,92 @@ describe('Utils Functions', () => {
expect(generatePreviewUrl(contentlet)).toBe('');
});
});

describe('initContentEditSessionStorage', () => {
let sessionStorageMock: { [key: string]: string };

beforeEach(() => {
sessionStorageMock = {};

// Mock sessionStorage
Object.defineProperty(window, 'sessionStorage', {
value: {
getItem: (key: string) => sessionStorageMock[key] || null,
setItem: (key: string, value: string) => {
sessionStorageMock[key] = value;
},
clear: () => {
sessionStorageMock = {};
}
},
writable: true
});
});

it('should initialize UI state with default values while preserving isSidebarOpen from storage', () => {
// Arrange
const storedState: UIState = {
activeTab: 2,
isSidebarOpen: false,
activeSidebarTab: 3
};
sessionStorageMock[UI_STORAGE_KEY] = JSON.stringify(storedState);

// Act
const result = initContentEditSessionStorage();

// Assert
expect(result).toEqual({
activeTab: 0,
isSidebarOpen: false, // Preserved from storage
activeSidebarTab: 0
});
});

it('should initialize UI state with default values when storage is empty', () => {
// Act
const result = initContentEditSessionStorage();

// Assert
expect(result).toEqual({
activeTab: 0,
isSidebarOpen: true, // Default value when no storage
activeSidebarTab: 0
});
});

it('should initialize UI state with default values when storage is invalid JSON', () => {
// Arrange
sessionStorageMock[UI_STORAGE_KEY] = 'invalid-json';

// Act
const result = initContentEditSessionStorage();

// Assert
expect(result).toEqual({
activeTab: 0,
isSidebarOpen: true, // Default value when storage is invalid
activeSidebarTab: 0
});
});

it('should handle storage with missing isSidebarOpen property', () => {
// Arrange
const storedState = {
activeTab: 2,
activeSidebarTab: 3
};
sessionStorageMock[UI_STORAGE_KEY] = JSON.stringify(storedState);

// Act
const result = initContentEditSessionStorage();

// Assert
expect(result).toEqual({
activeTab: 0,
isSidebarOpen: true, // Default value when property is missing
activeSidebarTab: 0
});
});
});
});
25 changes: 19 additions & 6 deletions core-web/libs/edit-content/src/lib/utils/functions.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,16 +313,16 @@ export const generatePreviewUrl = (contentlet: DotCMSContentlet): string => {
};

/**
* Gets the UI state from localStorage or returns the initial state if not found
* Gets the UI state from sessionStorage or returns the initial state if not found
*/
export const getStoredUIState = (): UIState => {
try {
const storedState = localStorage.getItem(UI_STORAGE_KEY);
const storedState = sessionStorage.getItem(UI_STORAGE_KEY);
if (storedState) {
return JSON.parse(storedState);
}
} catch (e) {
console.warn('Error reading UI state from localStorage:', e);
console.warn('Error reading UI state from sessionStorage:', e);
}

return {
Expand All @@ -333,12 +333,25 @@ export const getStoredUIState = (): UIState => {
};

/**
* Saves the UI state to localStorage
* Saves the UI state to sessionStorage
*/
export const saveStoreUIState = (state: UIState): void => {
try {
localStorage.setItem(UI_STORAGE_KEY, JSON.stringify(state));
sessionStorage.setItem(UI_STORAGE_KEY, JSON.stringify(state));
} catch (e) {
console.warn('Error saving UI state to localStorage:', e);
console.warn('Error saving UI state to sessionStorage:', e);
}
};

/**
* Initializes the UI state keeping only the isSidebarOpen value from storage
*/
export const initContentEditSessionStorage = (): UIState => {
const currentState = getStoredUIState();

return {
activeTab: 0,
isSidebarOpen: currentState?.isSidebarOpen ?? true,
activeSidebarTab: 0
};
};
Loading