musora-content-services 2.169.0-0 → 2.169.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -36
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +2 -2
- package/dist/services/api/types.js +2 -0
- package/package.json +2 -1
- package/src/filterBuilder.js +2 -2
- package/src/index.d.ts +25 -16
- package/src/index.js +25 -16
- package/src/lib/sanity/decorators/examples.ts +0 -3
- package/src/lib/sanity/decorators/need-lifetime-upgrade.ts +1 -1
- package/src/lib/sanity/filter.ts +4 -4
- package/src/services/endScreen/endScreen.ts +1 -2
- package/src/services/eventsAPI.js +1 -1
- package/src/services/forums/types.ts +0 -2
- package/src/services/{user → notifications}/notifications.js +3 -3
- package/src/services/notifications/public-announcements.ts +234 -0
- package/src/services/permissions/PermissionsAdapter.ts +0 -24
- package/src/services/permissions/PermissionsV1Adapter.ts +2 -2
- package/src/services/permissions/PermissionsV2Adapter.ts +2 -2
- package/src/services/permissions/permissions.ts +2 -2
- package/src/services/permissions/types.d.ts +0 -3
- package/src/services/sanity.js +5 -5
- package/src/services/user/types.d.ts +0 -1
- package/src/version-info.js +1 -1
|
@@ -1,39 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"Bash(
|
|
5
|
-
"Bash(
|
|
6
|
-
"
|
|
7
|
-
"Bash(counselors ls *)",
|
|
8
|
-
"Bash(counselors groups *)",
|
|
9
|
-
"Bash(counselors run *)",
|
|
4
|
+
"Bash(rg:*)",
|
|
5
|
+
"Bash(npm run lint:*)",
|
|
6
|
+
"Bash(ls:*)",
|
|
10
7
|
"Bash(npm test *)",
|
|
11
|
-
"Bash(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"Read(//tmp/pr-review-v2/**)",
|
|
15
|
-
"Bash(cat /home/alesevero/railenvironment/applications/musora-content-services/AGENTS.md)",
|
|
16
|
-
"Bash(echo \"no AGENTS.md\")",
|
|
17
|
-
"Bash(echo \"exit=$?\")",
|
|
18
|
-
"Bash(git checkout *)",
|
|
19
|
-
"Skill(pr)",
|
|
20
|
-
"Skill(create-decision)",
|
|
21
|
-
"mcp__github__pull_request_read",
|
|
22
|
-
"mcp__github__push_files",
|
|
23
|
-
"Bash(git push *)",
|
|
24
|
-
"mcp__github__create_pull_request",
|
|
25
|
-
"mcp__github__search_pull_requests",
|
|
26
|
-
"Bash(git commit *)",
|
|
27
|
-
"Bash(git add *)",
|
|
28
|
-
"mcp__github__list_pull_requests",
|
|
29
|
-
"mcp__sanity__get_schema",
|
|
30
|
-
"mcp__sanity__query_documents",
|
|
31
|
-
"Bash(git rm *)",
|
|
32
|
-
"Bash(git rebase *)"
|
|
33
|
-
]
|
|
8
|
+
"Bash(npx jest *)"
|
|
9
|
+
],
|
|
10
|
+
"deny": []
|
|
34
11
|
},
|
|
35
|
-
"model": "sonnet",
|
|
36
12
|
"enabledMcpjsonServers": [
|
|
13
|
+
"1password",
|
|
37
14
|
"atlassian",
|
|
38
15
|
"figma",
|
|
39
16
|
"google-workspace",
|
|
@@ -47,14 +24,10 @@
|
|
|
47
24
|
"chrome-devtools",
|
|
48
25
|
"railway",
|
|
49
26
|
"github",
|
|
50
|
-
"asana"
|
|
51
|
-
],
|
|
52
|
-
"disabledMcpjsonServers": [
|
|
27
|
+
"asana",
|
|
53
28
|
"nightwatch",
|
|
54
|
-
"1password",
|
|
55
29
|
"mysql-staging",
|
|
56
30
|
"mysql-local",
|
|
57
31
|
"mobile"
|
|
58
|
-
]
|
|
59
|
-
"effortLevel": "medium"
|
|
32
|
+
]
|
|
60
33
|
}
|
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
## [2.169.0
|
|
5
|
+
## [2.169.0](https://github.com/railroadmedia/musora-content-services/compare/v2.168.0...v2.169.0) (2026-08-04)
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
### Features
|
|
9
9
|
|
|
10
|
-
* **
|
|
10
|
+
* **BEHSTP-415:** MCS public announcement fetch fn's ([#1010](https://github.com/railroadmedia/musora-content-services/issues/1010)) ([d504fb1](https://github.com/railroadmedia/musora-content-services/commit/d504fb105ba5303bfe7327ddef24fe468bdec374))
|
|
11
11
|
|
|
12
12
|
## [2.168.0](https://github.com/railroadmedia/musora-content-services/compare/v2.167.6...v2.168.0) (2026-07-30)
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musora-content-services",
|
|
3
|
-
"version": "2.169.0
|
|
3
|
+
"version": "2.169.0",
|
|
4
4
|
"description": "A package for Musoras content services ",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"ts-jest": "^29.2.4"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@portabletext/to-html": "^2.0.14",
|
|
48
49
|
"@sanity/client": "^5.4.2",
|
|
49
50
|
"dayjs": "^1.11.13",
|
|
50
51
|
"docdash": "^2.0.2",
|
package/src/filterBuilder.js
CHANGED
|
@@ -95,7 +95,7 @@ export class FilterBuilder {
|
|
|
95
95
|
// This must be run before _applyPublishDateRestrictions()
|
|
96
96
|
if (this.bypassStatuses) return this
|
|
97
97
|
if (this.availableContentStatuses.length === 0) {
|
|
98
|
-
if (this.userData.
|
|
98
|
+
if (this.userData.isAdmin) {
|
|
99
99
|
this.availableContentStatuses = [
|
|
100
100
|
this.STATUS_DRAFT,
|
|
101
101
|
this.STATUS_SCHEDULED,
|
|
@@ -136,7 +136,7 @@ export class FilterBuilder {
|
|
|
136
136
|
if (this.bypassPermissions && !this.showOnlyOwnedContent) return this
|
|
137
137
|
const adapter = getPermissionsAdapter()
|
|
138
138
|
// Check if admin (admins bypass permissions)
|
|
139
|
-
if (adapter.
|
|
139
|
+
if (adapter.isAdmin(this.userData) && !this.showOnlyOwnedContent) return this
|
|
140
140
|
|
|
141
141
|
// Generate permissions filter using adapter
|
|
142
142
|
const permissionsFilter = adapter.generatePermissionsFilter(this.userData, {
|
package/src/index.d.ts
CHANGED
|
@@ -235,6 +235,28 @@ import {
|
|
|
235
235
|
updateMultiUserAccount
|
|
236
236
|
} from './services/multi-user-accounts/multi-user-accounts.ts';
|
|
237
237
|
|
|
238
|
+
import {
|
|
239
|
+
deleteAllNotifications,
|
|
240
|
+
deleteNotification,
|
|
241
|
+
fetchLiveEventPollingState,
|
|
242
|
+
fetchNotificationSettings,
|
|
243
|
+
fetchNotifications,
|
|
244
|
+
fetchUnreadCount,
|
|
245
|
+
markAllNotificationsAsRead,
|
|
246
|
+
markNotificationAsRead,
|
|
247
|
+
markNotificationAsUnread,
|
|
248
|
+
pauseLiveEventPolling,
|
|
249
|
+
restoreNotification,
|
|
250
|
+
startLiveEventPolling,
|
|
251
|
+
updateNotificationSetting
|
|
252
|
+
} from './services/notifications/notifications.js';
|
|
253
|
+
|
|
254
|
+
import {
|
|
255
|
+
blockContentToHtml,
|
|
256
|
+
fetchAllPublicAnnouncements,
|
|
257
|
+
fetchPublicAnnouncement
|
|
258
|
+
} from './services/notifications/public-announcements.ts';
|
|
259
|
+
|
|
238
260
|
import {
|
|
239
261
|
getRecentActivityOffline
|
|
240
262
|
} from './services/offline/activities.ts';
|
|
@@ -439,22 +461,6 @@ import {
|
|
|
439
461
|
upgradeSubscription
|
|
440
462
|
} from './services/user/memberships.ts';
|
|
441
463
|
|
|
442
|
-
import {
|
|
443
|
-
deleteAllNotifications,
|
|
444
|
-
deleteNotification,
|
|
445
|
-
fetchLiveEventPollingState,
|
|
446
|
-
fetchNotificationSettings,
|
|
447
|
-
fetchNotifications,
|
|
448
|
-
fetchUnreadCount,
|
|
449
|
-
markAllNotificationsAsRead,
|
|
450
|
-
markNotificationAsRead,
|
|
451
|
-
markNotificationAsUnread,
|
|
452
|
-
pauseLiveEventPolling,
|
|
453
|
-
restoreNotification,
|
|
454
|
-
startLiveEventPolling,
|
|
455
|
-
updateNotificationSetting
|
|
456
|
-
} from './services/user/notifications.js';
|
|
457
|
-
|
|
458
464
|
import {
|
|
459
465
|
getOnboardingRecommendedContent,
|
|
460
466
|
getOnboardingStatus,
|
|
@@ -534,6 +540,7 @@ declare module 'musora-content-services' {
|
|
|
534
540
|
applyCloudflareWrapper,
|
|
535
541
|
applySanityTransformations,
|
|
536
542
|
assignModeratorToComment,
|
|
543
|
+
blockContentToHtml,
|
|
537
544
|
blockUser,
|
|
538
545
|
blockedUsers,
|
|
539
546
|
buildEntityAndTotalQuery,
|
|
@@ -585,6 +592,7 @@ declare module 'musora-content-services' {
|
|
|
585
592
|
extractSanityUrl,
|
|
586
593
|
fetchAll,
|
|
587
594
|
fetchAllFilterOptions,
|
|
595
|
+
fetchAllPublicAnnouncements,
|
|
588
596
|
fetchArtistBySlug,
|
|
589
597
|
fetchArtistLessons,
|
|
590
598
|
fetchArtists,
|
|
@@ -649,6 +657,7 @@ declare module 'musora-content-services' {
|
|
|
649
657
|
fetchPost,
|
|
650
658
|
fetchPosts,
|
|
651
659
|
fetchPracticeGoals,
|
|
660
|
+
fetchPublicAnnouncement,
|
|
652
661
|
fetchRecent,
|
|
653
662
|
fetchRecentActivitiesActiveTabs,
|
|
654
663
|
fetchRecentUserActivities,
|
package/src/index.js
CHANGED
|
@@ -239,6 +239,28 @@ import {
|
|
|
239
239
|
updateMultiUserAccount
|
|
240
240
|
} from './services/multi-user-accounts/multi-user-accounts.ts';
|
|
241
241
|
|
|
242
|
+
import {
|
|
243
|
+
deleteAllNotifications,
|
|
244
|
+
deleteNotification,
|
|
245
|
+
fetchLiveEventPollingState,
|
|
246
|
+
fetchNotificationSettings,
|
|
247
|
+
fetchNotifications,
|
|
248
|
+
fetchUnreadCount,
|
|
249
|
+
markAllNotificationsAsRead,
|
|
250
|
+
markNotificationAsRead,
|
|
251
|
+
markNotificationAsUnread,
|
|
252
|
+
pauseLiveEventPolling,
|
|
253
|
+
restoreNotification,
|
|
254
|
+
startLiveEventPolling,
|
|
255
|
+
updateNotificationSetting
|
|
256
|
+
} from './services/notifications/notifications.js';
|
|
257
|
+
|
|
258
|
+
import {
|
|
259
|
+
blockContentToHtml,
|
|
260
|
+
fetchAllPublicAnnouncements,
|
|
261
|
+
fetchPublicAnnouncement
|
|
262
|
+
} from './services/notifications/public-announcements.ts';
|
|
263
|
+
|
|
242
264
|
import {
|
|
243
265
|
getRecentActivityOffline
|
|
244
266
|
} from './services/offline/activities.ts';
|
|
@@ -443,22 +465,6 @@ import {
|
|
|
443
465
|
upgradeSubscription
|
|
444
466
|
} from './services/user/memberships.ts';
|
|
445
467
|
|
|
446
|
-
import {
|
|
447
|
-
deleteAllNotifications,
|
|
448
|
-
deleteNotification,
|
|
449
|
-
fetchLiveEventPollingState,
|
|
450
|
-
fetchNotificationSettings,
|
|
451
|
-
fetchNotifications,
|
|
452
|
-
fetchUnreadCount,
|
|
453
|
-
markAllNotificationsAsRead,
|
|
454
|
-
markNotificationAsRead,
|
|
455
|
-
markNotificationAsUnread,
|
|
456
|
-
pauseLiveEventPolling,
|
|
457
|
-
restoreNotification,
|
|
458
|
-
startLiveEventPolling,
|
|
459
|
-
updateNotificationSetting
|
|
460
|
-
} from './services/user/notifications.js';
|
|
461
|
-
|
|
462
468
|
import {
|
|
463
469
|
getOnboardingRecommendedContent,
|
|
464
470
|
getOnboardingStatus,
|
|
@@ -533,6 +539,7 @@ export {
|
|
|
533
539
|
applyCloudflareWrapper,
|
|
534
540
|
applySanityTransformations,
|
|
535
541
|
assignModeratorToComment,
|
|
542
|
+
blockContentToHtml,
|
|
536
543
|
blockUser,
|
|
537
544
|
blockedUsers,
|
|
538
545
|
buildEntityAndTotalQuery,
|
|
@@ -584,6 +591,7 @@ export {
|
|
|
584
591
|
extractSanityUrl,
|
|
585
592
|
fetchAll,
|
|
586
593
|
fetchAllFilterOptions,
|
|
594
|
+
fetchAllPublicAnnouncements,
|
|
587
595
|
fetchArtistBySlug,
|
|
588
596
|
fetchArtistLessons,
|
|
589
597
|
fetchArtists,
|
|
@@ -648,6 +656,7 @@ export {
|
|
|
648
656
|
fetchPost,
|
|
649
657
|
fetchPosts,
|
|
650
658
|
fetchPracticeGoals,
|
|
659
|
+
fetchPublicAnnouncement,
|
|
651
660
|
fetchRecent,
|
|
652
661
|
fetchRecentActivitiesActiveTabs,
|
|
653
662
|
fetchRecentUserActivities,
|
|
@@ -22,7 +22,7 @@ export function lifetimeUpgradeDecorator(
|
|
|
22
22
|
return {
|
|
23
23
|
field: NEED_LIFETIME_UPGRADE_FIELD,
|
|
24
24
|
compute: (item) => {
|
|
25
|
-
if (userPermissions?.
|
|
25
|
+
if (userPermissions?.isAdmin || !hasLifetime || hasPlus) return false
|
|
26
26
|
if (!item.need_access) return false
|
|
27
27
|
return item.membership_tier === 'plus'
|
|
28
28
|
},
|
package/src/lib/sanity/filter.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface StatusConfig {
|
|
|
23
23
|
statuses?: string[]
|
|
24
24
|
bypassStatuses?: boolean
|
|
25
25
|
isSingle?: boolean
|
|
26
|
-
|
|
26
|
+
isAdmin?: boolean
|
|
27
27
|
prefix?: Prefix
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -349,7 +349,7 @@ export class Filters {
|
|
|
349
349
|
const adapter = getPermissionsAdapter()
|
|
350
350
|
const userData = config.userData || (await adapter.fetchUserPermissions())
|
|
351
351
|
|
|
352
|
-
if (adapter.
|
|
352
|
+
if (adapter.isAdmin(userData)) return ''
|
|
353
353
|
|
|
354
354
|
const permissionsFilter = adapter.generatePermissionsFilter(userData, {
|
|
355
355
|
prefix: config.prefix || '',
|
|
@@ -373,9 +373,9 @@ export class Filters {
|
|
|
373
373
|
// Auto-determine statuses if not provided
|
|
374
374
|
if (statuses.length === 0) {
|
|
375
375
|
const userData = await getPermissionsAdapter().fetchUserPermissions()
|
|
376
|
-
const
|
|
376
|
+
const isAdmin = getPermissionsAdapter().isAdmin(userData)
|
|
377
377
|
|
|
378
|
-
if (config.
|
|
378
|
+
if (config.isAdmin || isAdmin) {
|
|
379
379
|
statuses = [
|
|
380
380
|
STATUS_DRAFT,
|
|
381
381
|
STATUS_SCHEDULED,
|
|
@@ -34,8 +34,7 @@ export async function getEndScreen({
|
|
|
34
34
|
brand
|
|
35
35
|
}: GetEndScreenParams): Promise<EndScreenResult> {
|
|
36
36
|
const userData = await getUserData()
|
|
37
|
-
|
|
38
|
-
const isAdmin = userData?.has_all_content_access ?? false
|
|
37
|
+
const isAdmin = userData?.is_admin ?? false
|
|
39
38
|
|
|
40
39
|
if (playlist) {
|
|
41
40
|
const nextItem = next_item ??
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {fetchLiveEventPollingState, fetchUnreadCount, pauseLiveEventPolling} from "./
|
|
1
|
+
import {fetchLiveEventPollingState, fetchUnreadCount, pauseLiveEventPolling} from "./notifications/notifications.js"
|
|
2
2
|
import {fetchLiveEvent} from "./sanity"
|
|
3
3
|
import { DataContext, PollingStateVersionKey } from './dataContext'
|
|
4
4
|
|
|
@@ -10,7 +10,6 @@ export interface Liker {
|
|
|
10
10
|
access_level: string
|
|
11
11
|
liked_at: string
|
|
12
12
|
liked_at_diff: string | null
|
|
13
|
-
badge: string | null
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
export interface WhoLikedResponse {
|
|
@@ -28,7 +27,6 @@ export interface ForumUser {
|
|
|
28
27
|
profile_picture_url: string | null
|
|
29
28
|
access_level: string
|
|
30
29
|
signature: string | null
|
|
31
|
-
badge: string | null
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
export interface ForumPost {
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { GET, PUT, DELETE } from '../../infrastructure/http/HttpClient.ts'
|
|
5
5
|
import eventsAPI from '../eventsAPI.js'
|
|
6
|
-
import '
|
|
7
|
-
import {globalConfig} from "../config";
|
|
8
|
-
import {fetchLiveEvent} from "../sanity";
|
|
6
|
+
import '../user/types.js'
|
|
7
|
+
import {globalConfig} from "../config.js";
|
|
8
|
+
import {fetchLiveEvent} from "../sanity.js";
|
|
9
9
|
|
|
10
10
|
const baseUrl = `/api/notifications`
|
|
11
11
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { fetchSanity, getSanityDate } from '../sanity.js'
|
|
2
|
+
import { globalConfig } from '../config.js'
|
|
3
|
+
|
|
4
|
+
interface PublicAnnouncement {
|
|
5
|
+
title: string
|
|
6
|
+
slug: string
|
|
7
|
+
published_on: string
|
|
8
|
+
description: string
|
|
9
|
+
message: string
|
|
10
|
+
forum_data?: Record<string, ForumData>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ForumData {
|
|
14
|
+
category_id: string
|
|
15
|
+
forum_id: string
|
|
16
|
+
url: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetches a specific public announcement by slug, or by default the most recent published one.
|
|
21
|
+
* @param {string|null} slug
|
|
22
|
+
* @returns {Promise<PublicAnnouncement|null>} The public announcement object, or null if not found
|
|
23
|
+
*/
|
|
24
|
+
export async function fetchPublicAnnouncement(slug?: string): Promise<PublicAnnouncement | null> {
|
|
25
|
+
const now = getSanityDate(new Date())
|
|
26
|
+
|
|
27
|
+
const slugString = slug ? `&& slug.current == '${slug}'` : ''
|
|
28
|
+
|
|
29
|
+
// grab the specific one by slug, or the most recent one if no slug is provided
|
|
30
|
+
const query = `*[_type == 'public-announcement' && published_on <= '${now}' ${slugString}]
|
|
31
|
+
{
|
|
32
|
+
title,
|
|
33
|
+
'slug': slug.current,
|
|
34
|
+
published_on,
|
|
35
|
+
description,
|
|
36
|
+
message,
|
|
37
|
+
forum_data
|
|
38
|
+
}
|
|
39
|
+
| order(published_on desc)`
|
|
40
|
+
|
|
41
|
+
const result = await fetchSanity(query, false, {processNeedAccess: false, processPageType: false})
|
|
42
|
+
|
|
43
|
+
if (result?.message) {
|
|
44
|
+
result.message = blockContentToHtml(result.message)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fetches all public announcements published within the last `spanDays` days (default 1 year).
|
|
52
|
+
* @param {number|null} spanDays - Number of days to look back for public announcements (default: 1 year)
|
|
53
|
+
* @returns {Promise<PublicAnnouncement[]|null>} Array of public announcement objects, or null if none found
|
|
54
|
+
*/
|
|
55
|
+
export async function fetchAllPublicAnnouncements(spanDays: number = 365): Promise<PublicAnnouncement[] | null> {
|
|
56
|
+
const rawNowDate = new Date()
|
|
57
|
+
const rawNow = new Date().getTime()
|
|
58
|
+
const nowDate = getSanityDate(rawNowDate)
|
|
59
|
+
const startDate = getSanityDate(new Date(rawNow - spanDays * 24 * 60 * 60 * 1000))
|
|
60
|
+
|
|
61
|
+
const query = `*[_type == 'public-announcement' && published_on >= '${startDate}' && published_on <= '${nowDate}']
|
|
62
|
+
{
|
|
63
|
+
title,
|
|
64
|
+
'slug': slug.current,
|
|
65
|
+
published_on
|
|
66
|
+
}
|
|
67
|
+
| order(published_on desc)`
|
|
68
|
+
|
|
69
|
+
return await fetchSanity(query, true, {processNeedAccess: false, processPageType: false})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ListStackEntry {
|
|
73
|
+
tag: 'ul' | 'ol'
|
|
74
|
+
level: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {Array<Object>} blocks - Sanity Portable Text / block content array
|
|
79
|
+
* @returns {string} HTML string
|
|
80
|
+
*/
|
|
81
|
+
export function blockContentToHtml(blocks: any[]): string {
|
|
82
|
+
if (!blocks) {
|
|
83
|
+
return ''
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let html = ''
|
|
87
|
+
const listStack: ListStackEntry[] = []
|
|
88
|
+
|
|
89
|
+
for (const block of blocks) {
|
|
90
|
+
const listItem = block.listItem ?? null
|
|
91
|
+
|
|
92
|
+
if (!listItem) {
|
|
93
|
+
html += closeLists(listStack, 0)
|
|
94
|
+
html += block._type === 'image' ? renderImage(block) : renderBlock(block)
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const level = block.level ?? 1
|
|
99
|
+
const tag: 'ul' | 'ol' = listItem === 'number' ? 'ol' : 'ul'
|
|
100
|
+
|
|
101
|
+
while (
|
|
102
|
+
listStack.length &&
|
|
103
|
+
(listStack[listStack.length - 1].level > level ||
|
|
104
|
+
(listStack[listStack.length - 1].level === level && listStack[listStack.length - 1].tag !== tag))
|
|
105
|
+
) {
|
|
106
|
+
const closed = listStack.pop()
|
|
107
|
+
html += `</${closed.tag}>`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
while (!listStack.length || listStack[listStack.length - 1].level < level) {
|
|
111
|
+
listStack.push({ tag, level: listStack.length + 1 })
|
|
112
|
+
html += `<${tag}>`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
html += `<li>${renderInline(block)}</li>`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
html += closeLists(listStack, 0)
|
|
119
|
+
return html
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function closeLists(listStack: ListStackEntry[], downToLevel: number): string {
|
|
123
|
+
let html = ''
|
|
124
|
+
while (listStack.length && listStack[listStack.length - 1].level > downToLevel) {
|
|
125
|
+
const closed = listStack.pop()
|
|
126
|
+
html += `</${closed.tag}>`
|
|
127
|
+
}
|
|
128
|
+
return html
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderInline(block: any): string {
|
|
132
|
+
const markDefsByKey: Record<string, any> = {}
|
|
133
|
+
for (const markDef of block.markDefs ?? []) {
|
|
134
|
+
markDefsByKey[markDef._key] = markDef
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let inner = ''
|
|
138
|
+
for (const child of block.children ?? []) {
|
|
139
|
+
inner += renderSpan(child, markDefsByKey)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return inner === '' ? ' ' : inner
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderBlock(block: any): string {
|
|
146
|
+
const tag = ((): string => {
|
|
147
|
+
switch (block.style ?? 'normal') {
|
|
148
|
+
case 'h1':
|
|
149
|
+
return 'h1'
|
|
150
|
+
case 'h2':
|
|
151
|
+
return 'h2'
|
|
152
|
+
case 'h3':
|
|
153
|
+
return 'h3'
|
|
154
|
+
case 'h4':
|
|
155
|
+
return 'h4'
|
|
156
|
+
case 'blockquote':
|
|
157
|
+
return 'blockquote'
|
|
158
|
+
default:
|
|
159
|
+
return 'p'
|
|
160
|
+
}
|
|
161
|
+
})()
|
|
162
|
+
|
|
163
|
+
const inner = renderInline(block)
|
|
164
|
+
|
|
165
|
+
return `<${tag}>${inner}</${tag}>`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderSpan(child: any, markDefsByKey: Record<string, any>): string {
|
|
169
|
+
let text = escapeHtml(child.text ?? '')
|
|
170
|
+
text = text.replace(/\n/g, '<br />')
|
|
171
|
+
|
|
172
|
+
for (const mark of child.marks ?? []) {
|
|
173
|
+
switch (true) {
|
|
174
|
+
case mark === 'strong':
|
|
175
|
+
text = `<strong>${text}</strong>`
|
|
176
|
+
break
|
|
177
|
+
case mark === 'em':
|
|
178
|
+
text = `<em>${text}</em>`
|
|
179
|
+
break
|
|
180
|
+
case mark === 'underline':
|
|
181
|
+
text = `<u>${text}</u>`
|
|
182
|
+
break
|
|
183
|
+
case mark === 'code':
|
|
184
|
+
text = `<code>${text}</code>`
|
|
185
|
+
break
|
|
186
|
+
case markDefsByKey[mark] !== undefined:
|
|
187
|
+
text = renderMarkDef(markDefsByKey[mark], text)
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return text
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderMarkDef(markDef: any, text: string): string {
|
|
196
|
+
if (markDef?._type !== 'link') {
|
|
197
|
+
return text
|
|
198
|
+
}
|
|
199
|
+
const href = escapeHtml(markDef.href ?? '')
|
|
200
|
+
return `<a href="${href}">${text}</a>`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function renderImage(block: any): string {
|
|
204
|
+
const ref = block.asset?._ref ?? null
|
|
205
|
+
const percent = block.display_width_percent ?? 100
|
|
206
|
+
|
|
207
|
+
const url = sanityImageRefToUrl(ref)
|
|
208
|
+
if (!url) {
|
|
209
|
+
return ''
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return percent < 100
|
|
213
|
+
? `<img src="${url}" style="width:${percent}%;max-width:100%;" />`
|
|
214
|
+
: `<img src="${url}" />`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function escapeHtml(text: string): string {
|
|
218
|
+
return text
|
|
219
|
+
.replace(/&/g, '&')
|
|
220
|
+
.replace(/"/g, '"')
|
|
221
|
+
.replace(/'/g, ''')
|
|
222
|
+
.replace(/</g, '<')
|
|
223
|
+
.replace(/>/g, '>')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sanityImageRefToUrl(ref: string | null | undefined): string | null {
|
|
227
|
+
const match = ref?.match(/^image-([a-f0-9]+)-(\d+x\d+)-(\w+)$/)
|
|
228
|
+
if (!match) {
|
|
229
|
+
return null
|
|
230
|
+
}
|
|
231
|
+
const [, assetId, dimensions, format] = match
|
|
232
|
+
const { projectId, dataset } = globalConfig.sanityConfig
|
|
233
|
+
return `https://cdn.sanity.io/images/${projectId}/${dataset}/${assetId}-${dimensions}.${format}`
|
|
234
|
+
}
|
|
@@ -106,28 +106,4 @@ export abstract class PermissionsAdapter {
|
|
|
106
106
|
isModerator(userPermissions: UserPermissions): boolean {
|
|
107
107
|
return userPermissions?.isModerator ?? false
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
hasAllContentAccess(userPermissions: UserPermissions): boolean {
|
|
111
|
-
return userPermissions?.hasAllContentAccess ?? false
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Check if user is a forum moderator.
|
|
116
|
-
*
|
|
117
|
-
* @param userPermissions - The user's permissions
|
|
118
|
-
* @returns True if user is a forum moderator
|
|
119
|
-
*/
|
|
120
|
-
isForumModerator(userPermissions: UserPermissions): boolean {
|
|
121
|
-
return userPermissions?.isForumModerator ?? false
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Check if user is a comment moderator.
|
|
126
|
-
*
|
|
127
|
-
* @param userPermissions - The user's permissions
|
|
128
|
-
* @returns True if user is a comment moderator
|
|
129
|
-
*/
|
|
130
|
-
isCommentModerator(userPermissions: UserPermissions): boolean {
|
|
131
|
-
return userPermissions?.isCommentModerator ?? false
|
|
132
|
-
}
|
|
133
109
|
}
|
|
@@ -52,7 +52,7 @@ export class PermissionsV1Adapter extends PermissionsAdapter {
|
|
|
52
52
|
*/
|
|
53
53
|
doesUserNeedAccess(content: ContentItem, userPermissions: UserPermissions): boolean {
|
|
54
54
|
// Admins always have access
|
|
55
|
-
if (this.
|
|
55
|
+
if (this.isAdmin(userPermissions)) {
|
|
56
56
|
return false
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -102,7 +102,7 @@ export class PermissionsV1Adapter extends PermissionsAdapter {
|
|
|
102
102
|
} = options
|
|
103
103
|
|
|
104
104
|
// Admins bypass permission filter
|
|
105
|
-
if (this.
|
|
105
|
+
if (this.isAdmin(userPermissions)) {
|
|
106
106
|
return null
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -47,7 +47,7 @@ export class PermissionsV2Adapter extends PermissionsAdapter {
|
|
|
47
47
|
*/
|
|
48
48
|
doesUserNeedAccess(content: ContentItem, userPermissions: UserPermissions): boolean {
|
|
49
49
|
// Admins always have access
|
|
50
|
-
if (this.
|
|
50
|
+
if (this.isAdmin(userPermissions)) {
|
|
51
51
|
return false
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -108,7 +108,7 @@ export class PermissionsV2Adapter extends PermissionsAdapter {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Admins bypass permission filter
|
|
111
|
-
if (this.
|
|
111
|
+
if (this.isAdmin(userPermissions)) {
|
|
112
112
|
return null
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -38,7 +38,7 @@ export async function reset(): Promise<void> {
|
|
|
38
38
|
export function isUserFreeTier(userPermissions: UserPermissions): boolean {
|
|
39
39
|
const permissions = userPermissions.permissions
|
|
40
40
|
|
|
41
|
-
if (userPermissions.
|
|
41
|
+
if (userPermissions.isAdmin) return false
|
|
42
42
|
|
|
43
43
|
return permissions.includes(MEMBERSHIP_PERMISSIONS.free)
|
|
44
44
|
&& !permissions.some(p => [MEMBERSHIP_PERMISSIONS.base, MEMBERSHIP_PERMISSIONS.plus].includes(p))
|
|
@@ -46,7 +46,7 @@ export function isUserFreeTier(userPermissions: UserPermissions): boolean {
|
|
|
46
46
|
|
|
47
47
|
export function doesUserHaveMembership(userPermissions: UserPermissions): boolean {
|
|
48
48
|
const permissions = userPermissions.permissions
|
|
49
|
-
return userPermissions.
|
|
49
|
+
return userPermissions.isAdmin
|
|
50
50
|
|| permissions.includes(MEMBERSHIP_PERMISSIONS.base)
|
|
51
51
|
|| permissions.includes(MEMBERSHIP_PERMISSIONS.plus)
|
|
52
52
|
}
|
|
@@ -3,7 +3,4 @@ export interface UserPermissions {
|
|
|
3
3
|
isAdmin: boolean // Whether the user is an admin
|
|
4
4
|
isModerator: boolean // Whether the user is a moderator
|
|
5
5
|
isABasicMember: boolean // Whether the user has basic membership
|
|
6
|
-
hasAllContentAccess: boolean
|
|
7
|
-
isForumModerator: boolean
|
|
8
|
-
isCommentModerator: boolean
|
|
9
6
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -899,14 +899,14 @@ export async function fetchAllFilterOptions(
|
|
|
899
899
|
const progressFilter = progressIds ? `&& railcontent_id in [${progressIds.join(',')}]` : ''
|
|
900
900
|
const adapter = getPermissionsAdapter()
|
|
901
901
|
const userPermissionsData = await adapter.fetchUserPermissions()
|
|
902
|
-
const
|
|
902
|
+
const isAdmin = adapter.isAdmin(userPermissionsData)
|
|
903
903
|
|
|
904
904
|
const constructCommonFilter = (excludeFilter) => {
|
|
905
905
|
const filterWithoutOption = excludeFilter
|
|
906
906
|
? filtersToGroq(filters, excludeFilter)
|
|
907
907
|
: includedFieldsFilter
|
|
908
908
|
const statusFilter = ' && status == "published"'
|
|
909
|
-
const includeStatusFilter = !
|
|
909
|
+
const includeStatusFilter = !isAdmin && !['instructor', 'artist', 'genre'].includes(contentType)
|
|
910
910
|
|
|
911
911
|
return coachId
|
|
912
912
|
? `brand == '${brand}' && status == "published" && references(*[_type=='instructor' && railcontent_id == ${coachId}]._id) ${filterWithoutOption || ''} ${term ? ` && (title match "${term}" || album match "${term}" || artist->name match "${term}" || genre[]->name match "${term}")` : ''}`
|
|
@@ -2117,14 +2117,14 @@ export async function fetchTabData(
|
|
|
2117
2117
|
// Check if user is admin to determine available content statuses
|
|
2118
2118
|
const adapter = getPermissionsAdapter()
|
|
2119
2119
|
const userData = await adapter.fetchUserPermissions()
|
|
2120
|
-
const
|
|
2120
|
+
const isAdminORModerator = adapter.isAdmin(userData) || adapter.isModerator(userData)
|
|
2121
2121
|
|
|
2122
2122
|
const filterWithRestrictions = await new FilterBuilder(filter, {
|
|
2123
2123
|
showMembershipRestrictedContent: true,
|
|
2124
|
-
availableContentStatuses:
|
|
2124
|
+
availableContentStatuses: isAdminORModerator
|
|
2125
2125
|
? CONTENT_STATUSES.ADMIN_ALL
|
|
2126
2126
|
: CONTENT_STATUSES.PUBLISHED_ONLY,
|
|
2127
|
-
pullFutureContent:
|
|
2127
|
+
pullFutureContent: isAdminORModerator ? true : false,
|
|
2128
2128
|
}).buildFilter()
|
|
2129
2129
|
query = buildEntityAndTotalQuery(filterWithRestrictions, entityFieldsString, {
|
|
2130
2130
|
sortOrder: sortOrder,
|
package/src/version-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const MCS_VERSION = '2.169.0
|
|
1
|
+
export const MCS_VERSION = '2.169.0'
|