Skip to content
Merged
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
61 changes: 61 additions & 0 deletions .changeset/selfish-guests-hope.md
Original file line number Diff line number Diff line change
@@ -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 (
<MagicBell
apiKey="__MAGICBELL_API_KEY__"
userEmail="__MAGICBELL_USER_EMAIL__"
userKey="__MAGICBELL_USER_KEY__"
stores={stores}
defaultIsOpen
>
{(props) => <FloatingNotificationInbox height={450} tabs={tabs} {...props} />}
</MagicBell>
);
}
```

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
```
22 changes: 20 additions & 2 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,29 @@ function App() {
<MagicBell
serverURL="https://api.magicbell.dev"
apiKey="024b10085bb148d918afe3d92f42b1eba16ad0bd"
userEmail="stephan@magicbell.io"
userEmail="stephan@example.com"
locale={customLocale}
defaultIsOpen={true}
stores={[
{ id: 'default', defaultQueryParams: {} },
{ id: 'topic', defaultQueryParams: { topic: 'issue-1' } },
{ id: 'category', defaultQueryParams: { category: 'billing' } },
{ id: 'both', defaultQueryParams: { category: 'billing', topic: 'issue-1' } },
]}
>
{(props) => <FloatingNotificationInbox height={450} {...props} isOpen />}
{(props) => (
<FloatingNotificationInbox
tabs={[
{ storeId: 'default', label: 'default' },
{ storeId: 'topic', label: 'topic issue-1' },
{ storeId: 'category', label: 'category billing' },
{ storeId: 'both', label: 'topic and category' },
]}
height={450}
{...props}
isOpen
/>
)}
</MagicBell>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/scripts/generate-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/user-resources/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <string...>',
'--category <string>',
'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.',
)
.option('--topics <string...>', 'A filter on the notifications based on the topic.')
.option('--topic <string>', 'A filter on the notifications based on the topic.')
.action(async (opts, cmd) => {
const { data, options } = parseOptions(opts);

Expand All @@ -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 <string...>',
'--category <string>',
'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.',
)
.option('--topics <string...>', 'A filter on the notifications based on the topic.')
.option('--topic <string>', 'A filter on the notifications based on the topic.')
.action(async (opts, cmd) => {
const { data, options } = parseOptions(opts);

Expand Down Expand Up @@ -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 <string...>',
'--category <string>',
'A filter on the notifications based on the category. If you want to get uncategorized notifications, use the "uncategorized" value.',
)
.option('--topics <string...>', 'A filter on the notifications based on the topic.')
.option('--topic <string>', 'A filter on the notifications based on the topic.')
.option('--paginate', 'Make additional HTTP requests to fetch all pages of results')
.option('--max-items <number>', 'Maximum number of items to fetch', Number)
.action(async ({ paginate, maxItems, ...opts }, cmd) => {
Expand Down
8 changes: 4 additions & 4 deletions packages/magicbell/src/client/method.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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) {
Expand All @@ -35,7 +35,7 @@ function isForcedQueryParams(object) {
}

function getUrl(path: string, params: Record<string, string>, 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] || '',
);
}
Expand Down
9 changes: 4 additions & 5 deletions packages/magicbell/src/client/resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
66 changes: 21 additions & 45 deletions packages/magicbell/src/schemas/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},

Expand Down Expand Up @@ -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',
},
},

Expand Down Expand Up @@ -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',
},
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/examples/embeddable-inbox-tabs/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
4 changes: 2 additions & 2 deletions packages/playground/examples/react-inbox-tabs/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
4 changes: 1 addition & 3 deletions packages/playground/examples/shared/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion packages/playground/examples/vue-inbox-tabs/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Component = ({
}) => (
<div className="flex flex-row justify-center p-4">
<MagicBellProvider apiKey={apiKey} userEmail={userEmail} userKey={userKey} theme={theme} stores={stores}>
<MagicBell {...props}>
<MagicBell {...(props as any)}>
{(props) => (
<FloatingNotificationInbox
onAllRead={onAllRead}
Expand Down Expand Up @@ -160,7 +160,7 @@ export const WithSplitInbox = 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' },
Expand Down
Loading