memos/web/src/store/view.ts
Steven 5f7c758f38 fix(web): make layout and direction settings reactive in UI
Fixed issue #5194 where changing the layout (List/Masonry) or sort direction
didn't update the UI until page refresh. The root cause was that ViewState
fields weren't marked as MobX observables, so the UI didn't react to changes
even though values were being persisted to localStorage.

Solution: Added constructor to ViewState that marks orderByTimeAsc and layout
fields as observable, following the same pattern used in other stores like
MemoFilterState.

Fixes #5194

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-28 07:58:15 +08:00

137 lines
3 KiB
TypeScript

/**
* View Store
*
* Manages UI display preferences and layout settings.
* This is a client state store that persists to localStorage.
*/
import { makeObservable, observable } from "mobx";
import { StandardState } from "./base-store";
const LOCAL_STORAGE_KEY = "memos-view-setting";
/**
* Layout mode options
*/
export type LayoutMode = "LIST" | "MASONRY";
/**
* View store state
* Contains UI preferences for displaying memos
*/
class ViewState extends StandardState {
/**
* Sort order: true = ascending (oldest first), false = descending (newest first)
*/
orderByTimeAsc: boolean = false;
/**
* Display layout mode
* - LIST: Traditional vertical list
* - MASONRY: Pinterest-style grid layout
*/
layout: LayoutMode = "LIST";
constructor() {
super();
makeObservable(this, {
orderByTimeAsc: observable,
layout: observable,
});
}
/**
* Override setPartial to persist to localStorage
*/
setPartial(partial: Partial<ViewState>): void {
// Validate layout if provided
if (partial.layout !== undefined && !["LIST", "MASONRY"].includes(partial.layout)) {
console.warn(`Invalid layout "${partial.layout}", ignoring`);
return;
}
Object.assign(this, partial);
// Persist to localStorage
try {
localStorage.setItem(
LOCAL_STORAGE_KEY,
JSON.stringify({
orderByTimeAsc: this.orderByTimeAsc,
layout: this.layout,
}),
);
} catch (error) {
console.warn("Failed to persist view settings:", error);
}
}
}
/**
* View store instance
*/
const viewStore = (() => {
const state = new ViewState();
// Load from localStorage on initialization
try {
const cached = localStorage.getItem(LOCAL_STORAGE_KEY);
if (cached) {
const data = JSON.parse(cached);
// Validate and restore orderByTimeAsc
if (Object.hasOwn(data, "orderByTimeAsc")) {
state.orderByTimeAsc = Boolean(data.orderByTimeAsc);
}
// Validate and restore layout
if (Object.hasOwn(data, "layout") && ["LIST", "MASONRY"].includes(data.layout)) {
state.layout = data.layout as LayoutMode;
}
}
} catch (error) {
console.warn("Failed to load view settings from localStorage:", error);
}
/**
* Toggle sort order between ascending and descending
*/
const toggleSortOrder = (): void => {
state.setPartial({ orderByTimeAsc: !state.orderByTimeAsc });
};
/**
* Set the layout mode
*
* @param layout - The layout mode to set
*/
const setLayout = (layout: LayoutMode): void => {
state.setPartial({ layout });
};
/**
* Reset to default settings
*/
const resetToDefaults = (): void => {
state.setPartial({
orderByTimeAsc: false,
layout: "LIST",
});
};
/**
* Clear persisted settings
*/
const clearStorage = (): void => {
localStorage.removeItem(LOCAL_STORAGE_KEY);
};
return {
state,
toggleSortOrder,
setLayout,
resetToDefaults,
clearStorage,
};
})();
export default viewStore;