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 { useVersionsStore } from './versions.store';
import { useUsersStore } from './users.store';
import * as versionsApi from '@n8n/rest-api-client/api/versions';
import type { IVersionNotificationSettings } from '@n8n/api-types';
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 = {
enabled: true,
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 { computed, ref } from 'vue';
import { useSettingsStore } from './settings.store';
import { useUsersStore } from './users.store';
import { useStorage } from '@/composables/useStorage';
import { jsonParse } from 'n8n-workflow';
import { useTelemetry } from '@/composables/useTelemetry';
@@ -52,6 +53,7 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, () => {
const { showToast, showMessage } = useToast();
const uiStore = useUIStore();
const settingsStore = useSettingsStore();
const usersStore = useUsersStore();
const readWhatsNewArticlesStorage = useStorage(LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES);
const lastDismissedWhatsNewCalloutStorage = useStorage(LOCAL_STORAGE_DISMISSED_WHATS_NEW_CALLOUT);
@@ -169,9 +171,22 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, () => {
};
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),
);
return hasNewArticle && !allArticlesDismissed;
};
const fetchWhatsNew = async () => {
@@ -273,5 +288,7 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, () => {
isWhatsNewArticleRead,
setWhatsNewArticleRead,
closeWhatsNewCallout,
shouldShowWhatsNewCallout,
dismissWhatsNewCallout,
};
});