Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/calc2/components/editorBagalg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ export class EditorBagalg extends React.Component<Props, State> {
],
},
]}
clearOnReload={false}
clearOnNavigation={true}
/>
);
}
Expand Down
216 changes: 193 additions & 23 deletions src/calc2/components/editorBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,14 @@ type Props = {

exampleBags?: string,

exampleRA?: string
exampleRA?: string,

/** Optional TTL for restoring editor content (in ms). If omitted or 0, content never expires. */
contentTtlMs?: number,
/** If true, don't restore saved content on a page reload (soft or hard). */
clearOnReload?: boolean,
/** If true, clear saved content when opening the editor via in-app navigation (i.e. not a reload). */
clearOnNavigation?: boolean,
};

type State = {
Expand Down Expand Up @@ -685,7 +692,102 @@ class Relation {

const gutterClass = 'CodeMirror-table-edit-markers';
const eventExecSuccessfulName = 'editor.execSuccessful';
const HISTORY_STORAGE_KEY = "@editor/history";
const EDITOR_CONTENT_STORAGE_KEY = "@editor/content";

function getHistoryStorageKey(editorMode: string) {
return `${HISTORY_STORAGE_KEY}/${editorMode}`
}

function getEditorContentStorageKey(editorMode: string) {
return `${EDITOR_CONTENT_STORAGE_KEY}/${editorMode}`
}

/**
* Load editor content from storage.
* Supports legacy plain-string values and new JSON payloads { content, savedAt }.
* If ttlMs is provided (>0), will return empty string when content is older than ttlMs.
*/
function loadEditorContentFromStorage(storage: Storage, editorMode: string, ttlMs?: number): string {
const raw = storage.getItem(getEditorContentStorageKey(editorMode));
if (!raw) {
return '';
}
try {
const parsed = JSON.parse(raw);
if (typeof parsed === 'string') {
// legacy format: plain content string
return parsed;
}
const content = parsed?.content as string | undefined;
const savedAt = parsed?.savedAt as number | undefined;
if (!content) {
return '';
}
if (ttlMs && ttlMs > 0) {
if (!savedAt) {
return '';
}
if ((Date.now() - savedAt) > ttlMs) {
return '';
}
}
return content;
}
catch {
// not JSON, treat as legacy raw content
return raw;
}
}

/** Save content with timestamp for optional TTL support */
function saveEditorContentToStorage(content: string, editorMode: string, storage: Storage) {
const payload = JSON.stringify({ content, savedAt: Date.now() });
storage.setItem(getEditorContentStorageKey(editorMode), payload);
}

/** Best-effort detection whether current navigation is a reload (hard or soft). */
function isReloadNavigation(): boolean {
const navEntries = (performance.getEntriesByType && performance.getEntriesByType('navigation')) as PerformanceNavigationTiming[];
if (navEntries && navEntries.length > 0) {
return navEntries[0].type === 'reload';
}
// Fallback to deprecated API
// @ts-ignore
if (performance && performance.navigation) {
// 1 === TYPE_RELOAD
// @ts-ignore
return performance.navigation.type === 1;
}
return false;
}

function loadHistoryFromStorage(storage: Storage, editorMode: string): HistoryEntry[] {
const historyStr = storage.getItem(getHistoryStorageKey(editorMode))
if (!historyStr) {
return []
}
return (JSON.parse(historyStr) as (HistoryEntry & { time: string })[]).map(({ time, ...entry }) => ({ ...entry, time: new Date(time) }))
}

function appendHistoryToStorage(entry: HistoryEntry, historyMaxEntries: number, editorMode: string, storage: Storage) {
/**
* append history to LocalStorage with max entries
* preventing repeated code into it
*/
const history = loadHistoryFromStorage(storage, editorMode);
const historyWithoutCurrentEntry = history.filter((h) => h.code !== entry.code);
const updatedHistory = [
entry,
...historyWithoutCurrentEntry
].slice(0, historyMaxEntries);
storage.setItem(getHistoryStorageKey(editorMode), JSON.stringify(updatedHistory));
return updatedHistory;
}

function clearHistoryFromStorage(storage: Storage, editorMode: string) {
storage.removeItem(getHistoryStorageKey(editorMode));
}

export class EditorBase extends React.Component<Props, State> {
private hinterCache: {
Expand Down Expand Up @@ -779,7 +881,7 @@ export class EditorBase extends React.Component<Props, State> {
this.state = {
editor: null,
codeMirrorOptions,
history: [],
history: loadHistoryFromStorage(window.localStorage, props.tab || props.mode),
isSelectionSelected: false,
execSuccessful: false,
execErrors: [],
Expand Down Expand Up @@ -902,6 +1004,17 @@ export class EditorBase extends React.Component<Props, State> {

// setting example queries..
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, snapshot?: any) {
if (prevProps.mode !== this.props.mode || prevProps.tab !== this.props.tab) {
this.setState({ history: loadHistoryFromStorage(window.localStorage, this.props.tab || this.props.mode) })
// Load saved content when mode changes (respect optional TTL)
const { editor } = this.state;
if (editor) {
const savedContent = loadEditorContentFromStorage(window.localStorage, this.props.mode, this.props.contentTtlMs);
if (savedContent) {
editor.setValue(savedContent);
}
}
}
if(prevState.editor) {
if(this.props.exampleSql && this.props.exampleSql !== '' && !this.state.addedExampleSqlQuery && this.props.tab === 'sql') {
this.replaceAll(this.props.exampleSql)
Expand All @@ -920,6 +1033,15 @@ export class EditorBase extends React.Component<Props, State> {
}
}

onHistoryStorageChange(event: StorageEvent) {
const keyId = this.props.tab || this.props.mode;
if (event.storageArea === window.localStorage && event.key === getHistoryStorageKey(keyId)) {
this.setState({
history: loadHistoryFromStorage(event.storageArea, keyId)
});
}
}


componentDidMount() {
const node = findDOMNode(this) as Element;
Expand All @@ -929,6 +1051,25 @@ export class EditorBase extends React.Component<Props, State> {
}

const editor = CodeMirror.fromTextArea(textarea, this.state.codeMirrorOptions);

// Load saved content from local storage (respect optional TTL and reload/navigation preferences)
const reloaded = isReloadNavigation();
const contentKeyId = this.props.tab || this.props.mode;
// If clearOnReload and this is a reload -> clear and start empty
if (this.props.clearOnReload && reloaded) {
window.localStorage.removeItem(getEditorContentStorageKey(contentKeyId));
}
// If clearOnNavigation and this is NOT a reload (i.e. opened via in-app navigation) -> clear and start empty
else if (this.props.clearOnNavigation && !reloaded) {
window.localStorage.removeItem(getEditorContentStorageKey(contentKeyId));
}
else {
const savedContent = loadEditorContentFromStorage(window.localStorage, contentKeyId, this.props.contentTtlMs);
if (savedContent) {
editor.setValue(savedContent);
}
}

this.setState({
editor,
relationEditorName: '',
Expand All @@ -953,12 +1094,19 @@ export class EditorBase extends React.Component<Props, State> {

editor.on('change', (cm: CodeMirror.Editor) => {
this.props.textChange(cm);
// Save content to local storage whenever it changes
const contentKeyId = this.props.tab || this.props.mode;
saveEditorContentToStorage(cm.getValue(), contentKeyId, window.localStorage);
});


window.addEventListener("storage", this.onHistoryStorageChange);
}


componentWillUnmount(): void {
window.removeEventListener("storage", this.onHistoryStorageChange);
}

render() {
const {
execErrors,
Expand Down Expand Up @@ -1058,21 +1206,36 @@ export class EditorBase extends React.Component<Props, State> {
<div className="btn-group history-container">
<DropdownList
label={<span><FontAwesomeIcon icon={faHistory as IconProp} /> <span className="hideOnSM"><T id="calc.editors.button-history" /></span></span>}
elements={history.map(h => ({
label: (
<>
<small className="muted text-muted">{h.time.toLocaleTimeString()}</small>
<div>{h.code}</div>
{/*
// colorize the code
codeNode.addClass('colorize');
CodeMirror.colorize(codeNode, this.state.editor.getOption('mode'));
*/}
</>
),
value: h,
}))}
onChange={this.applyHistory}
elements={[
...history.map(h => ({
label: (
<>
<small className="muted text-muted">{h.time.toLocaleTimeString()}</small>
<div>{h.code}</div>
{/*
// colorize the code
codeNode.addClass('colorize');
CodeMirror.colorize(codeNode, this.state.editor.getOption('mode'));
*/}
</>
),
value: h,
})),
...(history.length > 0 ? [
{ type: 'separator' as const },
{
label: <span>{t('calc.editors.button-clear-history', { defaultValue: 'Clear history' })}</span>,
value: '__clear__',
}
] : [])
]}
onChange={(value: HistoryEntry | string) => {
if (value === '__clear__') {
this.clearHistory();
return;
}
this.applyHistory(value as HistoryEntry);
}}
/>
</div>
)
Expand Down Expand Up @@ -1155,21 +1318,28 @@ export class EditorBase extends React.Component<Props, State> {

historyAddEntry(code: string) {
const { historyMaxEntries = 10, historyMaxLabelLength = 20 } = this.props;
const keyId = this.props.tab || this.props.mode;

const entry = {
time: new Date(),
label: code.length > historyMaxLabelLength ? code.substr(0, historyMaxLabelLength - 4) + ' ...' : code,
code,
code: code.trim(),
};

this.setState({
history: [
entry,
...this.state.history,
].slice(0, historyMaxEntries),
history: appendHistoryToStorage(entry, historyMaxEntries, keyId, window.localStorage),
});
}

clearHistory = () => {
if (!window.confirm(t('calc.editors.confirm-clear-history', { defaultValue: 'Clear all history entries?' }))) {
return;
}
const keyId = this.props.tab || this.props.mode;
clearHistoryFromStorage(window.localStorage, keyId);
this.setState({ history: [] });
}

clearExecutionAlerts() {
this.state.execErrors.splice(0, this.state.execErrors.length);
toast.dismiss();
Expand Down
2 changes: 2 additions & 0 deletions src/calc2/components/editorGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ export class EditorGroup extends React.Component<Props> {
],
},
]}
clearOnReload={false}
clearOnNavigation={true}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/calc2/components/editorRelalg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ export class EditorRelalg extends React.Component<Props, State> {
],
},
]}
clearOnReload={false}
clearOnNavigation={true}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/calc2/components/editorSql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ export class EditorSql extends React.Component<Props> {
],
},
]}
clearOnReload={false}
clearOnNavigation={true}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/calc2/components/editorTrc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ export class EditorTrc extends React.Component<Props, State> {
],
},
]}
clearOnReload={false}
clearOnNavigation={true}
/>
);
}
Expand Down
5 changes: 4 additions & 1 deletion src/locales/.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
"calc.maintainer-groups.saarland": "",
"calc.maintainer-groups.ufes": "",
"calc.editors.button-history": "",
"calc.editors.button-clear-history": "",
"calc.editors.confirm-clear-history": "",
"calc.editors.insert-relation-title": "",
"calc.editors.insert-relation-tooltip": "",
"calc.editors.group.tab-name": "",
Expand Down Expand Up @@ -220,5 +222,6 @@
"calc.editors.ra.inline-editor.row-name": "",
"calc.editors.ra.inline-editor.row-type": "",
"calc.editors.ra.inline-editor.input-relation-name": "",
"calc.navigation.imprint": ""
"calc.navigation.imprint": "",
"local.change": ""
}
5 changes: 4 additions & 1 deletion src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@
"calc.maintainer-groups.ufes": "Bundesuniversit\u00e4t Espirito Santo",
"calc.maintainer-groups.savben": "Istituto di Istruzione Superiore Savoia Benincasa",
"calc.editors.button-history": "Verlauf",
"calc.editors.button-clear-history": "Verlauf l\u00f6schen",
"calc.editors.confirm-clear-history": "Sind Sie sicher, dass Sie den Verlauf l\u00f6schen m\u00f6chten?",
"calc.editors.insert-relation-title": "Einf\u00fcgen",
"calc.editors.insert-relation-tooltip": "Beziehungs- oder Spaltennamen einf\u00fcgen",
"calc.editors.group.tab-name": "Datensatz Editor",
Expand Down Expand Up @@ -277,5 +279,6 @@
"calc.editors.ra.button-zoom-out": "Herauszoomen",
"calc.editors.ra.button-zoom-reset": "Auf Standard-Zoomstufe zur\u00fccksetzen",
"calc.menu.recently-used": "Zuletzt verwendete Gists",
"calc.result.exec.time": "Ausführungszeit: "
"calc.result.exec.time": "Ausführungszeit: ",
"local.change": "Seite neu laden um Sprache zu \u00e4ndern?"
}
2 changes: 2 additions & 0 deletions src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ export const langDE: Partial<typeof langEN> = {
'calc.maintainer-groups.ufes': 'Bundesuniversität Espírito Santo',
'calc.maintainer-groups.savben': 'Istituto di Istruzione Superiore Savoia Benincasa',
'calc.editors.button-history': 'Verlauf',
'calc.editors.button-clear-history': 'Verlauf löschen',
'calc.editors.confirm-clear-history': 'Sind Sie sicher, dass Sie den Verlauf löschen möchten?',
'calc.editors.insert-relation-title': 'Einfügen',
'calc.editors.insert-relation-tooltip': 'Beziehungs- oder Spaltennamen einfügen',
'calc.editors.group.tab-name': 'Datensatz Editor',
Expand Down
5 changes: 4 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@
"calc.maintainer-groups.ufes": "Federal University of Esp\u00edrito Santo",
"calc.maintainer-groups.savben": "Istituto di Istruzione Superiore Savoia Benincasa",
"calc.editors.button-history": "history",
"calc.editors.button-clear-history": "clear",
"calc.editors.confirm-clear-history": "Are you sure you want to clear the history? This action cannot be undone.",
"calc.editors.insert-relation-title": "Insert",
"calc.editors.insert-relation-tooltip": "Insert relation or column names",
"calc.editors.group.tab-name": "Group Editor",
Expand Down Expand Up @@ -277,5 +279,6 @@
"calc.editors.ra.button-zoom-out": "Zoom out",
"calc.editors.ra.button-zoom-reset": "Reset zoom",
"calc.menu.recently-used": "Recently used gists",
"calc.result.exec.time": "Execution time: "
"calc.result.exec.time": "Execution time: ",
"local.change": "Reload page to change language?"
}
Loading