From a191ab8c1407ec8efda5ba976f80d6d9bc57e7d0 Mon Sep 17 00:00:00 2001 From: Stephan Meijer Date: Tue, 18 Jun 2024 15:11:25 +0200 Subject: [PATCH 1/2] feat: rename categories > category and topics > topic --- .changeset/selfish-guests-hope.md | 61 +++++++++++++++++ example/index.tsx | 18 ++++- packages/cli/scripts/generate-resources.ts | 2 +- .../cli/src/user-resources/notifications.ts | 12 ++-- packages/magicbell/src/client/method.ts | 8 +-- .../magicbell/src/client/resource.test.ts | 9 ++- .../magicbell/src/schemas/notifications.ts | 66 ++++++------------- .../angular-inbox-tabs/app.component.ts | 2 +- .../examples/embeddable-inbox-tabs/app.ts | 4 +- .../examples/react-inbox-tabs/app.tsx | 4 +- packages/playground/examples/shared/mocks.ts | 4 +- .../examples/vue-inbox-tabs/app.vue | 2 +- .../notifications/helpers/strategies.ts | 9 +-- .../types/INotificationsStoresCollection.ts | 6 +- .../FloatingNotificationInbox.stories.tsx | 4 +- .../src/components/MagicBell/MagicBell.tsx | 4 +- .../NotificationInbox.stories.tsx | 2 +- .../NotificationInbox.spec.tsx | 8 +-- 18 files changed, 135 insertions(+), 90 deletions(-) create mode 100644 .changeset/selfish-guests-hope.md diff --git a/.changeset/selfish-guests-hope.md b/.changeset/selfish-guests-hope.md new file mode 100644 index 000000000..8abcee0f3 --- /dev/null +++ b/.changeset/selfish-guests-hope.md @@ -0,0 +1,61 @@ +--- +'@magicbell/react-headless': major +'playground': major +'magicbell': major +'@magicbell/magicbell-react': major +'@magicbell/cli': major +--- + +**Breaking Change**! + +We've renamed the `categories` property to `category` and the `topics` property to `topic`, to reflect that these properties only support a single value. We haven't been supporting multiple categories or topics for a while now, and believe that renaming this property is the right thing to do. It requires a small change on your end, but the clear naming reduces the number of potential bugs caused by misunderstanding. + +If you make use of different stores or tabs using the `categories` or `topics` properties, you'll need to rename them to their singular variants. + +```diff +import MagicBell, { FloatingNotificationInbox } from '@magicbell/magicbell-react'; +import React from 'react'; + +const stores = [ + { id: 'default', defaultQueryParams: {} }, + { id: 'unread', defaultQueryParams: { read: false } }, +- { id: 'billing', defaultQueryParams: { categories: ['billing'] } }, ++ { id: 'billing', defaultQueryParams: { category: 'billing' } }, +- { id: 'support', defaultQueryParams: { topics: ['support'] } }, ++ { id: 'support', defaultQueryParams: { topic: 'support' } }, +]; + +const tabs = [ + { storeId: 'default', label: 'Latest' }, + { storeId: 'unread', label: 'Archive' }, + { storeId: 'billing', label: 'Billing' }, + { storeId: 'support', label: 'Issues' }, +]; + +export default function Index() { + return ( + + {(props) => } + + ); +} +``` + +Likewise, when you filter notifications using our cli, you might need to change some arguments: + +```diff +- magicbell user list notifications --topics support ++ magicbell user list notifications --topic support + +- magicbell user notifications mark-all-read --topics billing ++ magicbell user notifications mark-all-read --topic billing + +- magicbell user notifications mark-all-seen --topics other ++ magicbell user notifications mark-all-seen --topic other +``` diff --git a/example/index.tsx b/example/index.tsx index b49876a25..a5f152880 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -24,8 +24,24 @@ function App() { userEmail="stephan@magicbell.io" locale={customLocale} defaultIsOpen={true} + stores={[ + { id: 'default', defaultQueryParams: { category: 'billing' } }, + { id: 'other', defaultQueryParams: { topic: 'issue-1' } }, + { id: 'both', defaultQueryParams: { category: 'billing', topic: 'issue-1' } }, + ]} > - {(props) => } + {(props) => ( + + )} ); diff --git a/packages/cli/scripts/generate-resources.ts b/packages/cli/scripts/generate-resources.ts index 95cb18106..352bfeb5f 100644 --- a/packages/cli/scripts/generate-resources.ts +++ b/packages/cli/scripts/generate-resources.ts @@ -420,7 +420,7 @@ async function createResourceIndex(resources: Resource[]) { } async function main() { - const resources = await getResources(argv.spec || SPEC_URL); + const resources = await getResources(SPEC_URL); const projectResources = filterResourcesMethods(resources, (method) => hasHeader(method, { name: 'x-magicbell-api-secret', required: true }), diff --git a/packages/cli/src/user-resources/notifications.ts b/packages/cli/src/user-resources/notifications.ts index 12ce122b0..939c3ce0a 100644 --- a/packages/cli/src/user-resources/notifications.ts +++ b/packages/cli/src/user-resources/notifications.ts @@ -34,10 +34,10 @@ notifications 'A filter on the notifications based on the seen state. Specify false to select unseen notifications. Defaults to null.', ) .option( - '--categories ', + '--category ', 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.', ) - .option('--topics ', 'A filter on the notifications based on the topic.') + .option('--topic ', 'A filter on the notifications based on the topic.') .action(async (opts, cmd) => { const { data, options } = parseOptions(opts); @@ -61,10 +61,10 @@ notifications 'A filter on the notifications based on the seen state. Specify false to select unseen notifications. Defaults to null.', ) .option( - '--categories ', + '--category ', 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.', ) - .option('--topics ', 'A filter on the notifications based on the topic.') + .option('--topic ', 'A filter on the notifications based on the topic.') .action(async (opts, cmd) => { const { data, options } = parseOptions(opts); @@ -115,10 +115,10 @@ notifications 'A filter on the notifications based on the archived state. If false, only unarchived notifications will be returned. Defaults to null.', ) .option( - '--categories ', + '--category ', 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.', ) - .option('--topics ', 'A filter on the notifications based on the topic.') + .option('--topic ', 'A filter on the notifications based on the topic.') .option('--paginate', 'Make additional HTTP requests to fetch all pages of results') .option('--max-items ', 'Maximum number of items to fetch', Number) .action(async ({ paginate, maxItems, ...opts }, cmd) => { diff --git a/packages/magicbell/src/client/method.ts b/packages/magicbell/src/client/method.ts index c7ef81fdd..7e82d57b4 100644 --- a/packages/magicbell/src/client/method.ts +++ b/packages/magicbell/src/client/method.ts @@ -1,4 +1,4 @@ -import { isArray, isBoolean, isObject, isString, isStringArray } from '../lib/utils'; +import { isArray, isBoolean, isObject, isString } from '../lib/utils'; import { isOptionsHash } from './options'; import { ClientOptions, RequestMethod } from './types'; @@ -20,8 +20,8 @@ const queryParamValidators = { archived: isBoolean, read: isBoolean, seen: isBoolean, - categories: (value) => isString(value) || isStringArray(value), - topics: (value) => isString(value) || isStringArray(value), + category: isString, + topic: isString, }; function isForcedQueryParams(object) { @@ -35,7 +35,7 @@ function isForcedQueryParams(object) { } function getUrl(path: string, params: Record, options = { encode: true }) { - return path.replace(/{([\s\S]+?)}/g, ($0, $1) => + return path.replace(/{([\s\S]+?)}/g, (_, $1) => options.encode ? encodeURIComponent(params[$1] || '') : params[$1] || '', ); } diff --git a/packages/magicbell/src/client/resource.test.ts b/packages/magicbell/src/client/resource.test.ts index 109c3e777..7ce262e12 100644 --- a/packages/magicbell/src/client/resource.test.ts +++ b/packages/magicbell/src/client/resource.test.ts @@ -128,14 +128,13 @@ test('methods dont put categories and topics in query params if they hold object expect(await spy.lastRequest.json()).toEqual({ fake: { topics: [{ slug: 'issue.3' }] } }); expect(spy.lastRequest.url.search).toEqual(''); - await fakeResource.post({ categories: ['comments'] }); - + await fakeResource.post({ category: 'comments' }); expect(await spy.lastRequest.text()).toEqual(''); - expect(spy.lastRequest.url.search).toEqual('?categories=comments'); + expect(spy.lastRequest.url.search).toEqual('?category=comments'); - await fakeResource.post({ topics: ['issue.3', 'issue.4'] }); + await fakeResource.post({ topic: 'issue.3' }); expect(await spy.lastRequest.text()).toEqual(''); - expect(spy.lastRequest.url.search).toEqual('?topics=issue.3%2Cissue.4'); + expect(spy.lastRequest.url.search).toEqual('?topic=issue.3'); }); test('single resource methods are not iterable', async () => { diff --git a/packages/magicbell/src/schemas/notifications.ts b/packages/magicbell/src/schemas/notifications.ts index bc8415061..f340ef81d 100644 --- a/packages/magicbell/src/schemas/notifications.ts +++ b/packages/magicbell/src/schemas/notifications.ts @@ -25,25 +25,17 @@ export const MarkAllReadNotificationsPayloadSchema = { type: 'boolean', }, - categories: { - title: 'categories', + category: { + title: 'category', description: - 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.\nThe value can be either an array of strings or a comma-separated string.', - type: 'array', - - items: { - type: 'string', - }, + 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.', + type: 'string', }, - topics: { - title: 'topics', + topic: { + title: 'topic', description: 'A filter on the notifications based on the topic.', - type: 'array', - - items: { - type: 'string', - }, + type: 'string', }, }, @@ -77,25 +69,17 @@ export const MarkAllSeenNotificationsPayloadSchema = { type: 'boolean', }, - categories: { - title: 'categories', + category: { + title: 'category', description: - 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.\nThe value can be either an array of strings or a comma-separated string.', - type: 'array', - - items: { - type: 'string', - }, + 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.', + type: 'string', }, - topics: { - title: 'topics', + topic: { + title: 'topic', description: 'A filter on the notifications based on the topic.', - type: 'array', - - items: { - type: 'string', - }, + type: 'string', }, }, @@ -334,25 +318,17 @@ export const ListNotificationsPayloadSchema = { type: 'boolean', }, - categories: { - title: 'categories', + category: { + title: 'category', description: - 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.\nThe value can be either an array of strings or a comma-separated string.', - type: 'array', - - items: { - type: 'string', - }, + 'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.', + type: 'string', }, - topics: { - title: 'topics', + topic: { + title: 'topic', description: 'A filter on the notifications based on the topic.', - type: 'array', - - items: { - type: 'string', - }, + type: 'string', }, }, diff --git a/packages/playground/examples/angular-inbox-tabs/app.component.ts b/packages/playground/examples/angular-inbox-tabs/app.component.ts index 78c8da22c..3d670d18d 100644 --- a/packages/playground/examples/angular-inbox-tabs/app.component.ts +++ b/packages/playground/examples/angular-inbox-tabs/app.component.ts @@ -12,7 +12,7 @@ export class AppComponent implements AfterViewInit { const stores = [ { id: 'default', defaultQueryParams: {} }, { id: 'unread', defaultQueryParams: { read: true } }, - { id: 'billing', defaultQueryParams: { categories: ['billing'] } }, + { id: 'billing', defaultQueryParams: { category: 'billing' } }, ]; const tabs = [ diff --git a/packages/playground/examples/embeddable-inbox-tabs/app.ts b/packages/playground/examples/embeddable-inbox-tabs/app.ts index 1c18ead4f..8ec0cecee 100644 --- a/packages/playground/examples/embeddable-inbox-tabs/app.ts +++ b/packages/playground/examples/embeddable-inbox-tabs/app.ts @@ -7,8 +7,8 @@ const options = { height: 500, stores: [ { id: 'default', defaultQueryParams: {} }, - { id: 'unread', defaultQueryParams: { read: true } }, - { id: 'billing', defaultQueryParams: { categories: ['billing'] } }, + { id: 'unread', defaultQueryParams: { read: false } }, + { id: 'billing', defaultQueryParams: { category: 'billing' } }, ], tabs: [ { storeId: 'default', label: 'Latest' }, diff --git a/packages/playground/examples/react-inbox-tabs/app.tsx b/packages/playground/examples/react-inbox-tabs/app.tsx index 5311090af..1513fa8da 100644 --- a/packages/playground/examples/react-inbox-tabs/app.tsx +++ b/packages/playground/examples/react-inbox-tabs/app.tsx @@ -3,8 +3,8 @@ import React from 'react'; const stores = [ { id: 'default', defaultQueryParams: {} }, - { id: 'unread', defaultQueryParams: { read: true } }, - { id: 'billing', defaultQueryParams: { categories: ['billing'] } }, + { id: 'unread', defaultQueryParams: { read: false } }, + { id: 'billing', defaultQueryParams: { category: 'billing' } }, ]; const tabs = [ diff --git a/packages/playground/examples/shared/mocks.ts b/packages/playground/examples/shared/mocks.ts index b747908a8..47fb60744 100644 --- a/packages/playground/examples/shared/mocks.ts +++ b/packages/playground/examples/shared/mocks.ts @@ -168,9 +168,7 @@ const fakeNotifications = { }; addHandler('get', '/notifications', ({ params }) => { - const category = Array.isArray(params.categories) - ? params.categories[Math.floor(Math.random() * params.categories.length)] - : null; + const category: any = params.category || null; let notifications = fakeNotifications[category] || fakeNotifications.latest; diff --git a/packages/playground/examples/vue-inbox-tabs/app.vue b/packages/playground/examples/vue-inbox-tabs/app.vue index b3de55cee..05621c6f5 100644 --- a/packages/playground/examples/vue-inbox-tabs/app.vue +++ b/packages/playground/examples/vue-inbox-tabs/app.vue @@ -8,7 +8,7 @@ import { renderWidget } from '@magicbell/embeddable/dist/magicbell.esm.js'; const stores = [ { id: 'default', defaultQueryParams: {} }, { id: 'unread', defaultQueryParams: { read: true } }, - { id: 'billing', defaultQueryParams: { categories: ['billing'] } }, + { id: 'billing', defaultQueryParams: { category: 'billing' } }, ]; const tabs = [ diff --git a/packages/react-headless/src/stores/notifications/helpers/strategies.ts b/packages/react-headless/src/stores/notifications/helpers/strategies.ts index 94866f812..2d1870d6f 100644 --- a/packages/react-headless/src/stores/notifications/helpers/strategies.ts +++ b/packages/react-headless/src/stores/notifications/helpers/strategies.ts @@ -7,10 +7,6 @@ function eq(value, other) { return value === other || (value !== value && other !== other); } -function ensureArray(value) { - return Array.isArray(value) ? value : String(value).split(','); -} - export type NotificationCompareStrategy = ( notification: IRemoteNotification, context: Record, @@ -41,9 +37,8 @@ export function objMatchesContext( (attr === 'read' && !comparator(!isNil(notification.readAt), condition)) || (attr === 'seen' && !comparator(!isNil(notification.seenAt), condition)) || (attr === 'archived' && !comparator(!isNil(notification.archivedAt), condition)) || - (attr === 'categories' && - ensureArray(condition).every((category) => !comparator(notification.category, category))) || - (attr === 'topics' && ensureArray(condition).every((topic) => !comparator(notification.topic, topic))) || + (attr === 'category' && !comparator(notification.category, condition)) || + (attr === 'topic' && !comparator(notification.topic, condition)) || (Object.hasOwnProperty.call(notification, attr) && !comparator(notification[attr], condition)) ) { diff.push(attr); diff --git a/packages/react-headless/src/types/INotificationsStoresCollection.ts b/packages/react-headless/src/types/INotificationsStoresCollection.ts index 7b1d73f92..e38bf3219 100644 --- a/packages/react-headless/src/types/INotificationsStoresCollection.ts +++ b/packages/react-headless/src/types/INotificationsStoresCollection.ts @@ -16,14 +16,14 @@ export type QueryParams = { */ archived?: boolean; /** - * A filter on the notifications based on category. Use "uncategorized" to + * A filter on the notifications based on category. Use "uncategorized" * to target notifications without a category. */ - categories?: string[]; + category?: string; /** * A filter on the notifications based on topic. */ - topics?: string[]; + topic?: string; /** * A limit on the number of notifications to be returned. */ diff --git a/packages/react/src/components/FloatingNotificationInbox/FloatingNotificationInbox.stories.tsx b/packages/react/src/components/FloatingNotificationInbox/FloatingNotificationInbox.stories.tsx index 067e4fbb5..40e8e814d 100644 --- a/packages/react/src/components/FloatingNotificationInbox/FloatingNotificationInbox.stories.tsx +++ b/packages/react/src/components/FloatingNotificationInbox/FloatingNotificationInbox.stories.tsx @@ -21,7 +21,7 @@ const Component = ({ }) => (
- + {(props) => ( >; diff --git a/packages/react/src/components/NotificationInbox/NotificationInbox.stories.tsx b/packages/react/src/components/NotificationInbox/NotificationInbox.stories.tsx index b88bbb74c..d9c36d426 100644 --- a/packages/react/src/components/NotificationInbox/NotificationInbox.stories.tsx +++ b/packages/react/src/components/NotificationInbox/NotificationInbox.stories.tsx @@ -57,7 +57,7 @@ export const SplitInbox = merge(Default, { stores: [ { id: 'default', defaultQueryParams: {} }, { id: 'unread', defaultQueryParams: { read: true } }, - { id: 'billing', defaultQueryParams: { categories: ['billing'] } }, + { id: 'billing', defaultQueryParams: { category: 'billing' } }, ], tabs: [ { storeId: 'default', label: 'Latest' }, diff --git a/packages/react/tests/src/components/NotificationInbox/NotificationInbox.spec.tsx b/packages/react/tests/src/components/NotificationInbox/NotificationInbox.spec.tsx index 818ec9062..c8b769938 100644 --- a/packages/react/tests/src/components/NotificationInbox/NotificationInbox.spec.tsx +++ b/packages/react/tests/src/components/NotificationInbox/NotificationInbox.spec.tsx @@ -159,8 +159,8 @@ test('can render with a custom notification preferences component', async () => test('can render with multiple inbox tabs, and active tab changes when clicked', async () => { const stores = [ { id: 'default', defaultQueryParams: {} }, - { id: 'comments', defaultQueryParams: { read: true, categories: ['comments'] } }, - { id: 'billing', defaultQueryParams: { categories: ['billing'] } }, + { id: 'comments', defaultQueryParams: { read: true, category: 'comments' } }, + { id: 'billing', defaultQueryParams: { category: 'billing' } }, ]; const tabs = [ @@ -185,7 +185,7 @@ test('can render with multiple inbox tabs, and active tab changes when clicked', test('renders notifications matching selected tab', async () => { const stores = [ { id: 'default', defaultQueryParams: {} }, - { id: 'comments', defaultQueryParams: { read: true, categories: ['comments'] } }, + { id: 'comments', defaultQueryParams: { read: true, category: 'comments' } }, ]; const tabs = [ @@ -198,7 +198,7 @@ test('renders notifications matching selected tab', async () => { notifications: [ { ...fake.notification, - content: `notification in ${req.url.searchParams.get('categories') || 'default'} tab`, + content: `notification in ${req.url.searchParams.get('category') || 'default'} tab`, }, ], })); From 98fb87a703c8dee527a6de7cbc56e92e7a6c01d9 Mon Sep 17 00:00:00 2001 From: Stephan Meijer Date: Fri, 30 Aug 2024 11:40:47 +0200 Subject: [PATCH 2/2] chore: update example --- example/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/example/index.tsx b/example/index.tsx index a5f152880..bdcf85dfa 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -21,12 +21,13 @@ function App() { @@ -34,8 +35,9 @@ function App() {