feat(editor): New users see whatsnew notification only if new (#17409)

This commit is contained in:
Nikhil Kuriakose
2025-07-21 14:32:18 +02:00
committed by GitHub
parent 1cd5808846
commit a1d2a55f7e
2 changed files with 104 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { useVersionsStore } from './versions.store'; import { useVersionsStore } from './versions.store';
import { useUsersStore } from './users.store';
import * as versionsApi from '@n8n/rest-api-client/api/versions'; import * as versionsApi from '@n8n/rest-api-client/api/versions';
import type { IVersionNotificationSettings } from '@n8n/api-types'; import type { IVersionNotificationSettings } from '@n8n/api-types';
import type { Version, WhatsNewArticle, WhatsNewSection } from '@n8n/rest-api-client/api/versions'; import type { Version, WhatsNewArticle, WhatsNewSection } from '@n8n/rest-api-client/api/versions';
@@ -18,6 +19,8 @@ vi.mock('@/composables/useToast', () => {
}; };
}); });
vi.mock('./users.store');
const settings: IVersionNotificationSettings = { const settings: IVersionNotificationSettings = {
enabled: true, enabled: true,
endpoint: 'https://test.api.n8n.io/api/versions/', endpoint: 'https://test.api.n8n.io/api/versions/',
@@ -505,3 +508,86 @@ describe('versions.store', () => {
}); });
}); });
}); });
describe('shouldShowWhatsNewCallout', () => {
let versionsStore: ReturnType<typeof useVersionsStore>;
const makeArticle = (overrides: Partial<WhatsNewArticle> = {}): WhatsNewArticle => ({
id: 1,
title: 'Test',
content: 'Content',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
publishedAt: new Date().toISOString(),
...overrides,
});
beforeEach(() => {
localStorage.clear();
setActivePinia(createPinia());
});
it('returns false if there are no articles', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as any);
versionsStore = useVersionsStore();
Object.defineProperty(versionsStore, 'lastDismissedWhatsNewCallout', { get: () => [] });
versionsStore.whatsNew.items = [];
expect(versionsStore.shouldShowWhatsNewCallout()).toBe(false);
});
it('returns true if user has no createdAt and not all articles are dismissed', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as any);
versionsStore = useVersionsStore();
Object.defineProperty(versionsStore, 'lastDismissedWhatsNewCallout', { get: () => [] });
versionsStore.whatsNew.items = [makeArticle()];
expect(versionsStore.shouldShowWhatsNewCallout()).toBe(true);
});
it('returns false if all articles are dismissed', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as any);
versionsStore = useVersionsStore();
versionsStore.whatsNew.items = [makeArticle()];
versionsStore.dismissWhatsNewCallout();
expect(versionsStore.shouldShowWhatsNewCallout()).toBe(false);
});
it('returns true if user createdAt is before article updatedAt', () => {
const now = Date.now();
vi.mocked(useUsersStore).mockReturnValue({
currentUser: { createdAt: new Date(now - 10000).toISOString() },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
versionsStore = useVersionsStore();
Object.defineProperty(versionsStore, 'lastDismissedWhatsNewCallout', { get: () => [] });
versionsStore.whatsNew.items = [makeArticle({ updatedAt: new Date(now).toISOString() })];
expect(versionsStore.shouldShowWhatsNewCallout()).toBe(true);
});
it('returns false if user createdAt is after article updatedAt', () => {
const now = Date.now();
vi.mocked(useUsersStore).mockReturnValue({
currentUser: { createdAt: new Date(now).toISOString() },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
versionsStore = useVersionsStore();
Object.defineProperty(versionsStore, 'lastDismissedWhatsNewCallout', { get: () => [] });
versionsStore.whatsNew.items = [
makeArticle({ updatedAt: new Date(now - 10000).toISOString() }),
];
expect(versionsStore.shouldShowWhatsNewCallout()).toBe(false);
});
it('handles missing updatedAt on article', () => {
vi.mocked(useUsersStore).mockReturnValue({
currentUser: { createdAt: new Date().toISOString() },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
versionsStore = useVersionsStore();
Object.defineProperty(versionsStore, 'lastDismissedWhatsNewCallout', { get: () => [] });
versionsStore.whatsNew.items = [makeArticle({ updatedAt: undefined })];
expect(versionsStore.shouldShowWhatsNewCallout()).toBe(false);
});
});

View File

@@ -15,6 +15,7 @@ import { useToast } from '@/composables/useToast';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useSettingsStore } from './settings.store'; import { useSettingsStore } from './settings.store';
import { useUsersStore } from './users.store';
import { useStorage } from '@/composables/useStorage'; import { useStorage } from '@/composables/useStorage';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
@@ -52,6 +53,7 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, () => {
const { showToast, showMessage } = useToast(); const { showToast, showMessage } = useToast();
const uiStore = useUIStore(); const uiStore = useUIStore();
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const usersStore = useUsersStore();
const readWhatsNewArticlesStorage = useStorage(LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES); const readWhatsNewArticlesStorage = useStorage(LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES);
const lastDismissedWhatsNewCalloutStorage = useStorage(LOCAL_STORAGE_DISMISSED_WHATS_NEW_CALLOUT); const lastDismissedWhatsNewCalloutStorage = useStorage(LOCAL_STORAGE_DISMISSED_WHATS_NEW_CALLOUT);
@@ -169,9 +171,22 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, () => {
}; };
const shouldShowWhatsNewCallout = (): boolean => { const shouldShowWhatsNewCallout = (): boolean => {
return !whatsNewArticles.value.every((item) => const createdAt = usersStore.currentUser?.createdAt;
let hasNewArticle = false;
if (createdAt) {
const userCreatedAt = new Date(createdAt).getTime();
hasNewArticle = whatsNewArticles.value.some((item) => {
const updatedAt = item.updatedAt ? new Date(item.updatedAt).getTime() : 0;
return updatedAt > userCreatedAt;
});
} else {
hasNewArticle = true;
}
const allArticlesDismissed = whatsNewArticles.value.every((item) =>
lastDismissedWhatsNewCallout.value.includes(item.id), lastDismissedWhatsNewCallout.value.includes(item.id),
); );
return hasNewArticle && !allArticlesDismissed;
}; };
const fetchWhatsNew = async () => { const fetchWhatsNew = async () => {
@@ -273,5 +288,7 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, () => {
isWhatsNewArticleRead, isWhatsNewArticleRead,
setWhatsNewArticleRead, setWhatsNewArticleRead,
closeWhatsNewCallout, closeWhatsNewCallout,
shouldShowWhatsNewCallout,
dismissWhatsNewCallout,
}; };
}); });