@codingfactory/socialkit-vue 0.2.0 → 0.3.1

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.
@@ -0,0 +1,1670 @@
1
+ /**
2
+ * Generic circles store factory for SocialKit-powered frontends.
3
+ */
4
+ import { defineStore } from 'pinia';
5
+ const DEFAULT_FILTER_KEY = 'all';
6
+ const DEFAULT_MANAGEMENT_COUNTS = {
7
+ members: 0,
8
+ requests_pending: 0,
9
+ reports_pending: 0,
10
+ roles: 0,
11
+ bans_active: 0,
12
+ mutes_active: 0,
13
+ };
14
+ const defaultPaginationState = () => ({
15
+ cursor: null,
16
+ hasMore: true,
17
+ });
18
+ const emptyPaginationState = () => ({
19
+ cursor: null,
20
+ hasMore: false,
21
+ });
22
+ const toCursorPaginationState = (response) => ({
23
+ cursor: response.meta?.pagination?.next_cursor ?? null,
24
+ hasMore: response.meta?.pagination?.has_more ?? Boolean(response.meta?.pagination?.next_cursor),
25
+ });
26
+ const normalizeFilters = (filters) => {
27
+ const normalized = {};
28
+ if (typeof filters.visibility === 'string' && filters.visibility.length > 0) {
29
+ normalized.visibility = filters.visibility;
30
+ }
31
+ if (typeof filters.membership === 'string' && filters.membership.length > 0) {
32
+ normalized.membership = filters.membership;
33
+ }
34
+ if (typeof filters.search === 'string') {
35
+ const search = filters.search.trim();
36
+ if (search.length > 0) {
37
+ normalized.search = search;
38
+ }
39
+ }
40
+ return normalized;
41
+ };
42
+ const buildFilterKey = (filters) => {
43
+ const normalized = normalizeFilters(filters);
44
+ const entries = Object.entries(normalized)
45
+ .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey));
46
+ if (entries.length === 0) {
47
+ return DEFAULT_FILTER_KEY;
48
+ }
49
+ return entries.map(([key, value]) => `${key}:${value}`).join('|');
50
+ };
51
+ const mergeUniqueCircles = (existing, incoming) => {
52
+ const existingIds = new Set(existing.map(circle => circle.id));
53
+ const merged = [...existing];
54
+ for (const circle of incoming) {
55
+ if (existingIds.has(circle.id)) {
56
+ continue;
57
+ }
58
+ existingIds.add(circle.id);
59
+ merged.push(circle);
60
+ }
61
+ return merged;
62
+ };
63
+ const isRecord = (value) => {
64
+ return typeof value === 'object' && value !== null;
65
+ };
66
+ const readRecord = (value, key) => {
67
+ if (!isRecord(value)) {
68
+ return null;
69
+ }
70
+ const entry = value[key];
71
+ return isRecord(entry) ? entry : null;
72
+ };
73
+ const readString = (value) => {
74
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
75
+ };
76
+ const readNullableString = (value) => {
77
+ if (value === null || value === undefined) {
78
+ return null;
79
+ }
80
+ return readString(value);
81
+ };
82
+ const readBoolean = (value) => {
83
+ return typeof value === 'boolean' ? value : null;
84
+ };
85
+ const readNumber = (value) => {
86
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
87
+ };
88
+ const isCircleVisibility = (value) => {
89
+ return value === 'public' || value === 'closed' || value === 'secret';
90
+ };
91
+ const isCircleEntry = (value) => {
92
+ if (!isRecord(value)) {
93
+ return false;
94
+ }
95
+ return typeof value.id === 'string'
96
+ && value.id.length > 0
97
+ && typeof value.name === 'string'
98
+ && value.name.length > 0
99
+ && isCircleVisibility(value.visibility);
100
+ };
101
+ const sanitizeCirclesPayload = (payload) => {
102
+ if (!Array.isArray(payload)) {
103
+ return [];
104
+ }
105
+ return payload.filter(isCircleEntry);
106
+ };
107
+ const sanitizeCircleCollection = (circles) => {
108
+ return sanitizeCirclesPayload(circles);
109
+ };
110
+ const sanitizePermissionKeys = (value) => {
111
+ if (!Array.isArray(value)) {
112
+ return [];
113
+ }
114
+ return value.filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
115
+ };
116
+ const normalizeRoleDefinition = (value) => {
117
+ if (!isRecord(value)) {
118
+ return null;
119
+ }
120
+ const id = readString(value.id);
121
+ const slug = readString(value.slug);
122
+ const name = readString(value.name);
123
+ if (id === null || slug === null || name === null) {
124
+ return null;
125
+ }
126
+ return {
127
+ id,
128
+ slug,
129
+ name,
130
+ description: readNullableString(value.description),
131
+ color: readNullableString(value.color),
132
+ is_system: readBoolean(value.is_system) ?? false,
133
+ is_assignable: readBoolean(value.is_assignable) ?? true,
134
+ member_count: readNumber(value.member_count) ?? 0,
135
+ permissions: sanitizePermissionKeys(value.permissions),
136
+ };
137
+ };
138
+ const sanitizeRoleDefinitions = (value) => {
139
+ if (!Array.isArray(value)) {
140
+ return [];
141
+ }
142
+ return value
143
+ .map((entry) => normalizeRoleDefinition(entry))
144
+ .filter((entry) => entry !== null);
145
+ };
146
+ const normalizePermissionCatalogEntry = (value) => {
147
+ if (!isRecord(value)) {
148
+ return null;
149
+ }
150
+ const key = readString(value.key);
151
+ const label = readString(value.label);
152
+ const description = readString(value.description);
153
+ const module = readString(value.module);
154
+ const section = readString(value.section);
155
+ if (key === null || label === null || description === null || module === null || section === null) {
156
+ return null;
157
+ }
158
+ return {
159
+ key,
160
+ label,
161
+ description,
162
+ module,
163
+ owner_only: readBoolean(value.owner_only) ?? false,
164
+ surfaced: readBoolean(value.surfaced) ?? false,
165
+ section,
166
+ };
167
+ };
168
+ const sanitizePermissionCatalog = (value) => {
169
+ if (!Array.isArray(value)) {
170
+ return [];
171
+ }
172
+ return value
173
+ .map((entry) => normalizePermissionCatalogEntry(entry))
174
+ .filter((entry) => entry !== null);
175
+ };
176
+ const sanitizeManagementCounts = (value) => {
177
+ const counts = { ...DEFAULT_MANAGEMENT_COUNTS };
178
+ if (!isRecord(value)) {
179
+ return counts;
180
+ }
181
+ for (const [key, entry] of Object.entries(value)) {
182
+ const numericValue = readNumber(entry);
183
+ if (numericValue !== null) {
184
+ counts[key] = numericValue;
185
+ }
186
+ }
187
+ return counts;
188
+ };
189
+ const extractItems = (value) => {
190
+ if (Array.isArray(value)) {
191
+ return value;
192
+ }
193
+ if (!isRecord(value)) {
194
+ return [];
195
+ }
196
+ const data = readRecord(value, 'data');
197
+ const dataItems = data?.items;
198
+ if (Array.isArray(dataItems)) {
199
+ return dataItems;
200
+ }
201
+ const rootItems = value.items;
202
+ if (Array.isArray(rootItems)) {
203
+ return rootItems;
204
+ }
205
+ const rootData = value.data;
206
+ if (Array.isArray(rootData)) {
207
+ return rootData;
208
+ }
209
+ return [];
210
+ };
211
+ const toUnknownCursorPaginationState = (value) => {
212
+ if (!isRecord(value)) {
213
+ return emptyPaginationState();
214
+ }
215
+ const meta = readRecord(value, 'meta');
216
+ const pagination = readRecord(meta, 'pagination');
217
+ const nextCursor = readNullableString(pagination?.next_cursor);
218
+ return {
219
+ cursor: nextCursor,
220
+ hasMore: readBoolean(pagination?.has_more) ?? (nextCursor !== null),
221
+ };
222
+ };
223
+ const extractRoleDefinitions = (value) => {
224
+ if (isRecord(value) && 'roles' in value) {
225
+ return extractRoleDefinitions(value.roles);
226
+ }
227
+ return sanitizeRoleDefinitions(extractItems(value));
228
+ };
229
+ const extractManagementCounts = (value) => {
230
+ return isRecord(value) ? sanitizeManagementCounts(value.counts) : { ...DEFAULT_MANAGEMENT_COUNTS };
231
+ };
232
+ const extractManagementPermissionCatalog = (value) => {
233
+ return isRecord(value) ? sanitizePermissionCatalog(value.permission_catalog) : [];
234
+ };
235
+ const extractAssignedRoles = (member) => {
236
+ return sanitizeRoleDefinitions(member.assigned_roles);
237
+ };
238
+ const buildEffectivePermissions = (assignedRoles) => {
239
+ return [...new Set(assignedRoles.flatMap((role) => role.permissions))];
240
+ };
241
+ const deriveDisplayRole = (member, assignedRoles = extractAssignedRoles(member)) => {
242
+ const membershipRole = member.membership_role ?? (member.role === 'owner' ? 'owner' : 'member');
243
+ if (membershipRole === 'owner') {
244
+ return 'owner';
245
+ }
246
+ if (assignedRoles.some((role) => role.slug === 'group-admin')) {
247
+ return 'admin';
248
+ }
249
+ if (assignedRoles.length > 0) {
250
+ return 'moderator';
251
+ }
252
+ return member.role === 'owner' ? 'owner' : member.role;
253
+ };
254
+ const deriveRoleAssignmentsFromMembers = (members) => {
255
+ return members.map((member) => {
256
+ const roleAssignment = {
257
+ id: member.id,
258
+ user_id: member.user_id,
259
+ role: deriveDisplayRole(member),
260
+ assigned_at: member.joined_at,
261
+ };
262
+ if (member.user) {
263
+ roleAssignment.user = member.user;
264
+ }
265
+ return roleAssignment;
266
+ });
267
+ };
268
+ const getRoleStoreService = (circlesService) => {
269
+ // The shared service layer is mid-migration; runtime method checks let the store
270
+ // support both the legacy member-role API and the new RBAC role-definition API.
271
+ return circlesService;
272
+ };
273
+ export function createCirclesStoreDefinition(config) {
274
+ const { storeId = 'circles', circlesService, getErrorMessage, persist, } = config;
275
+ return defineStore(storeId, {
276
+ state: () => ({
277
+ circles: [],
278
+ currentCircle: null,
279
+ members: {},
280
+ joinRequests: {},
281
+ joinRequestsPagination: {},
282
+ managementActors: {},
283
+ managementSections: {},
284
+ managementBootstrapLoading: {},
285
+ managementBootstrapError: {},
286
+ managementSectionLoading: {},
287
+ managementSectionErrors: {},
288
+ managementBootstrappedAt: {},
289
+ roleDefinitions: {},
290
+ roleDefinitionsPagination: {},
291
+ managementCounts: {},
292
+ managementPermissionCatalog: {},
293
+ roleAssignments: {},
294
+ roleAssignmentsPagination: {},
295
+ reports: {},
296
+ reportsPagination: {},
297
+ bans: {},
298
+ bansPagination: {},
299
+ mutes: {},
300
+ mutesPagination: {},
301
+ auditLog: {},
302
+ auditLogPagination: {},
303
+ automodRules: {},
304
+ automodRulesPagination: {},
305
+ pendingRequestCircleIds: [],
306
+ loading: false,
307
+ error: null,
308
+ pagination: defaultPaginationState(),
309
+ activeFilterKey: DEFAULT_FILTER_KEY,
310
+ circlesByFilter: {
311
+ [DEFAULT_FILTER_KEY]: []
312
+ },
313
+ paginationByFilter: {
314
+ [DEFAULT_FILTER_KEY]: defaultPaginationState()
315
+ },
316
+ fetchedFilterKeys: {},
317
+ membersPagination: {},
318
+ filters: {}
319
+ }),
320
+ getters: {
321
+ userCircles: (state) => sanitizeCircleCollection(state.circles).filter(c => c.is_member || c.user_role === 'owner'),
322
+ joinableCircles: (state) => sanitizeCircleCollection(state.circles).filter(c => c.visibility === 'public' && !c.is_member && c.can_join),
323
+ managedCircles: (state) => sanitizeCircleCollection(state.circles).filter(c => c.user_role === 'owner' || c.can_manage),
324
+ filteredCircles: (state) => {
325
+ let filtered = [...sanitizeCircleCollection(state.circles)];
326
+ if (state.filters.membership === 'joined') {
327
+ filtered = filtered.filter(c => c.is_member || c.user_role === 'owner');
328
+ }
329
+ else if (state.filters.membership === 'managed') {
330
+ filtered = filtered.filter(c => c.user_role === 'owner' || c.can_manage);
331
+ }
332
+ else if (state.filters.membership === 'available') {
333
+ filtered = filtered.filter(c => !c.is_member && c.can_join);
334
+ }
335
+ if (state.filters.visibility) {
336
+ filtered = filtered.filter(c => c.visibility === state.filters.visibility);
337
+ }
338
+ if (state.filters.search) {
339
+ const searchTerm = state.filters.search.toLowerCase();
340
+ filtered = filtered.filter(c => c.name.toLowerCase().includes(searchTerm) ||
341
+ (c.description && c.description.toLowerCase().includes(searchTerm)));
342
+ }
343
+ return filtered;
344
+ },
345
+ getCircleById: (state) => (id) => {
346
+ return sanitizeCircleCollection(state.circles).find(c => c.id === id) || null;
347
+ },
348
+ getCircleMembers: (state) => (circleId) => {
349
+ return state.members[circleId] || [];
350
+ },
351
+ getMemberCount: (state) => (circleId) => {
352
+ const circle = sanitizeCircleCollection(state.circles).find(c => c.id === circleId);
353
+ return circle?.member_count || 0;
354
+ },
355
+ getManagementActor: (state) => (circleId) => {
356
+ return state.managementActors[circleId] ?? null;
357
+ },
358
+ getManagementSections: (state) => (circleId) => {
359
+ return state.managementSections[circleId] ?? [];
360
+ },
361
+ getRoleDefinitions: (state) => (circleId) => {
362
+ return state.roleDefinitions[circleId] ?? [];
363
+ },
364
+ getManagementCounts: (state) => (circleId) => {
365
+ return state.managementCounts[circleId] ?? { ...DEFAULT_MANAGEMENT_COUNTS };
366
+ },
367
+ getManagementPermissionCatalog: (state) => (circleId) => {
368
+ return state.managementPermissionCatalog[circleId] ?? [];
369
+ },
370
+ getRoleAssignments: (state) => (circleId) => {
371
+ return state.roleAssignments[circleId] ?? [];
372
+ },
373
+ getModerationReports: (state) => (circleId) => {
374
+ return state.reports[circleId] ?? [];
375
+ },
376
+ getBanRecords: (state) => (circleId) => {
377
+ return state.bans[circleId] ?? [];
378
+ },
379
+ getMuteRecords: (state) => (circleId) => {
380
+ return state.mutes[circleId] ?? [];
381
+ },
382
+ getAuditLogEntries: (state) => (circleId) => {
383
+ return state.auditLog[circleId] ?? [];
384
+ },
385
+ getAutomodRules: (state) => (circleId) => {
386
+ return state.automodRules[circleId] ?? [];
387
+ }
388
+ },
389
+ actions: {
390
+ setFilters(filters) {
391
+ const currentKey = this.activeFilterKey;
392
+ this.circlesByFilter[currentKey] = [...this.circles];
393
+ this.paginationByFilter[currentKey] = { ...this.pagination };
394
+ const nextFilters = normalizeFilters(filters);
395
+ const nextKey = buildFilterKey(nextFilters);
396
+ this.filters = nextFilters;
397
+ this.activeFilterKey = nextKey;
398
+ const cachedCircles = this.circlesByFilter[nextKey];
399
+ const cachedPagination = this.paginationByFilter[nextKey];
400
+ if (cachedCircles) {
401
+ this.circles = sanitizeCircleCollection(cachedCircles);
402
+ }
403
+ else {
404
+ this.circles = [];
405
+ this.circlesByFilter[nextKey] = [];
406
+ }
407
+ if (cachedPagination) {
408
+ this.pagination = { ...cachedPagination };
409
+ }
410
+ else {
411
+ const defaultPagination = defaultPaginationState();
412
+ this.pagination = defaultPagination;
413
+ this.paginationByFilter[nextKey] = { ...defaultPagination };
414
+ }
415
+ },
416
+ clearFilters() {
417
+ this.setFilters({});
418
+ },
419
+ hasFetchedCurrentFilter() {
420
+ return this.fetchedFilterKeys[this.activeFilterKey] === true;
421
+ },
422
+ ensureManagementState(circleId) {
423
+ if (!this.managementSectionLoading[circleId]) {
424
+ this.managementSectionLoading[circleId] = {};
425
+ }
426
+ if (!this.managementSectionErrors[circleId]) {
427
+ this.managementSectionErrors[circleId] = {};
428
+ }
429
+ if (!this.roleDefinitionsPagination[circleId]) {
430
+ this.roleDefinitionsPagination[circleId] = emptyPaginationState();
431
+ }
432
+ if (!this.managementCounts[circleId]) {
433
+ this.managementCounts[circleId] = { ...DEFAULT_MANAGEMENT_COUNTS };
434
+ }
435
+ if (!this.managementPermissionCatalog[circleId]) {
436
+ this.managementPermissionCatalog[circleId] = [];
437
+ }
438
+ if (!this.roleAssignmentsPagination[circleId]) {
439
+ this.roleAssignmentsPagination[circleId] = emptyPaginationState();
440
+ }
441
+ if (!this.reportsPagination[circleId]) {
442
+ this.reportsPagination[circleId] = emptyPaginationState();
443
+ }
444
+ if (!this.bansPagination[circleId]) {
445
+ this.bansPagination[circleId] = emptyPaginationState();
446
+ }
447
+ if (!this.mutesPagination[circleId]) {
448
+ this.mutesPagination[circleId] = emptyPaginationState();
449
+ }
450
+ if (!this.auditLogPagination[circleId]) {
451
+ this.auditLogPagination[circleId] = emptyPaginationState();
452
+ }
453
+ if (!this.automodRulesPagination[circleId]) {
454
+ this.automodRulesPagination[circleId] = emptyPaginationState();
455
+ }
456
+ if (!this.joinRequestsPagination[circleId]) {
457
+ this.joinRequestsPagination[circleId] = emptyPaginationState();
458
+ }
459
+ },
460
+ syncRoleAssignmentCompatibility(circleId) {
461
+ this.ensureManagementState(circleId);
462
+ this.roleAssignments[circleId] = deriveRoleAssignmentsFromMembers(this.members[circleId] ?? []);
463
+ this.roleAssignmentsPagination[circleId] = emptyPaginationState();
464
+ },
465
+ setRoleDefinitionsState(circleId, roles, pagination) {
466
+ this.ensureManagementState(circleId);
467
+ this.roleDefinitions[circleId] = [...roles];
468
+ this.roleDefinitionsPagination[circleId] = { ...pagination };
469
+ this.managementCounts[circleId] = {
470
+ ...DEFAULT_MANAGEMENT_COUNTS,
471
+ ...(this.managementCounts[circleId] ?? {}),
472
+ roles: roles.length,
473
+ };
474
+ },
475
+ upsertRoleDefinitionState(circleId, role) {
476
+ this.ensureManagementState(circleId);
477
+ const existingRoles = this.roleDefinitions[circleId] ?? [];
478
+ const existingIndex = existingRoles.findIndex((candidate) => candidate.id === role.id);
479
+ const nextRoles = [...existingRoles];
480
+ if (existingIndex === -1) {
481
+ nextRoles.unshift(role);
482
+ }
483
+ else {
484
+ nextRoles.splice(existingIndex, 1, role);
485
+ }
486
+ this.setRoleDefinitionsState(circleId, nextRoles, this.roleDefinitionsPagination[circleId] ?? emptyPaginationState());
487
+ },
488
+ removeRoleDefinitionState(circleId, roleId) {
489
+ this.ensureManagementState(circleId);
490
+ const nextRoles = (this.roleDefinitions[circleId] ?? []).filter((role) => role.id !== roleId);
491
+ this.setRoleDefinitionsState(circleId, nextRoles, this.roleDefinitionsPagination[circleId] ?? emptyPaginationState());
492
+ },
493
+ updateMemberRoleState(circleId, userId, patch) {
494
+ const members = this.members[circleId];
495
+ if (!members) {
496
+ return;
497
+ }
498
+ const memberIndex = members.findIndex((member) => member.user_id === userId || member.user?.id === userId);
499
+ if (memberIndex === -1) {
500
+ return;
501
+ }
502
+ const member = members[memberIndex];
503
+ if (!member) {
504
+ return;
505
+ }
506
+ members[memberIndex] = {
507
+ ...member,
508
+ ...patch,
509
+ };
510
+ this.syncRoleAssignmentCompatibility(circleId);
511
+ },
512
+ applyMemberRoleAssignments(circleId, userId, assignedRoles, effectivePermissions) {
513
+ const members = this.members[circleId];
514
+ if (!members) {
515
+ return;
516
+ }
517
+ const member = members.find((candidate) => candidate.user_id === userId || candidate.user?.id === userId);
518
+ if (!member) {
519
+ return;
520
+ }
521
+ const membershipRole = member.membership_role ?? (member.role === 'owner' ? 'owner' : 'member');
522
+ const displayRole = membershipRole === 'owner'
523
+ ? 'owner'
524
+ : deriveDisplayRole(member, assignedRoles);
525
+ this.updateMemberRoleState(circleId, userId, {
526
+ membership_role: membershipRole,
527
+ display_role: displayRole,
528
+ role: displayRole,
529
+ assigned_roles: assignedRoles,
530
+ effective_permissions: effectivePermissions ?? buildEffectivePermissions(assignedRoles),
531
+ });
532
+ },
533
+ setManagementSectionLoading(circleId, key, loading) {
534
+ this.ensureManagementState(circleId);
535
+ this.managementSectionLoading[circleId] = {
536
+ ...this.managementSectionLoading[circleId],
537
+ [key]: loading,
538
+ };
539
+ },
540
+ setManagementSectionError(circleId, key, error) {
541
+ this.ensureManagementState(circleId);
542
+ this.managementSectionErrors[circleId] = {
543
+ ...this.managementSectionErrors[circleId],
544
+ [key]: error,
545
+ };
546
+ },
547
+ applyCircleSnapshot(circle) {
548
+ const existingIndex = this.circles.findIndex((candidate) => candidate.id === circle.id || candidate.slug === circle.slug);
549
+ if (existingIndex === -1) {
550
+ this.circles.unshift(circle);
551
+ }
552
+ else {
553
+ this.circles.splice(existingIndex, 1, circle);
554
+ }
555
+ if (this.currentCircle?.id === circle.id || this.currentCircle?.slug === circle.slug) {
556
+ this.currentCircle = circle;
557
+ }
558
+ this.circlesByFilter[this.activeFilterKey] = [...this.circles];
559
+ if (circle.actor) {
560
+ this.managementActors[circle.id] = circle.actor;
561
+ this.managementSections[circle.id] = [...circle.actor.managementSections];
562
+ }
563
+ },
564
+ applyManagementBootstrap(bootstrap) {
565
+ const circleId = bootstrap.circle.id;
566
+ this.ensureManagementState(circleId);
567
+ this.applyCircleSnapshot(bootstrap.circle);
568
+ this.currentCircle = bootstrap.circle;
569
+ this.managementActors[circleId] = bootstrap.actor;
570
+ this.managementSections[circleId] = [...bootstrap.management_sections];
571
+ this.managementCounts[circleId] = extractManagementCounts(bootstrap);
572
+ this.managementPermissionCatalog[circleId] = extractManagementPermissionCatalog(bootstrap);
573
+ this.managementBootstrapError[circleId] = null;
574
+ this.managementBootstrappedAt[circleId] = new Date().toISOString();
575
+ this.members[circleId] = [...bootstrap.members.data];
576
+ this.membersPagination[circleId] = toCursorPaginationState(bootstrap.members);
577
+ this.joinRequests[circleId] = [...bootstrap.requests.data];
578
+ this.joinRequestsPagination[circleId] = toCursorPaginationState(bootstrap.requests);
579
+ this.setRoleDefinitionsState(circleId, extractRoleDefinitions(bootstrap), toUnknownCursorPaginationState(isRecord(bootstrap) ? bootstrap.roles : null));
580
+ this.syncRoleAssignmentCompatibility(circleId);
581
+ this.reports[circleId] = [...bootstrap.reports.data];
582
+ this.reportsPagination[circleId] = toCursorPaginationState(bootstrap.reports);
583
+ this.bans[circleId] = [...bootstrap.bans.data];
584
+ this.bansPagination[circleId] = toCursorPaginationState(bootstrap.bans);
585
+ this.mutes[circleId] = [...bootstrap.mutes.data];
586
+ this.mutesPagination[circleId] = toCursorPaginationState(bootstrap.mutes);
587
+ this.auditLog[circleId] = [...bootstrap.audit.data];
588
+ this.auditLogPagination[circleId] = toCursorPaginationState(bootstrap.audit);
589
+ this.automodRules[circleId] = [...bootstrap.automod.data];
590
+ this.automodRulesPagination[circleId] = toCursorPaginationState(bootstrap.automod);
591
+ this.managementSectionErrors[circleId] = {};
592
+ },
593
+ async fetchCircles(refresh = false) {
594
+ const filterKey = this.activeFilterKey || buildFilterKey(this.filters);
595
+ this.activeFilterKey = filterKey;
596
+ if (!this.circlesByFilter[filterKey]) {
597
+ this.circlesByFilter[filterKey] = [];
598
+ }
599
+ if (!this.paginationByFilter[filterKey]) {
600
+ this.paginationByFilter[filterKey] = defaultPaginationState();
601
+ }
602
+ if (refresh) {
603
+ const defaultPagination = defaultPaginationState();
604
+ this.circlesByFilter[filterKey] = [];
605
+ this.paginationByFilter[filterKey] = defaultPagination;
606
+ this.circles = [];
607
+ this.pagination = { ...defaultPagination };
608
+ }
609
+ const cursorState = this.paginationByFilter[filterKey];
610
+ if (!refresh && !cursorState.hasMore) {
611
+ return;
612
+ }
613
+ this.loading = true;
614
+ this.error = null;
615
+ try {
616
+ const response = await circlesService.list(cursorState.cursor, this.filters);
617
+ const sanitizedResponseData = sanitizeCirclesPayload(response.data);
618
+ const nextCircles = refresh
619
+ ? [...sanitizedResponseData]
620
+ : mergeUniqueCircles(sanitizeCircleCollection(this.circlesByFilter[filterKey]), sanitizedResponseData);
621
+ const nextPagination = {
622
+ cursor: response.meta?.pagination?.next_cursor || null,
623
+ hasMore: response.meta?.pagination?.has_more ?? Boolean(response.meta?.pagination?.next_cursor),
624
+ };
625
+ this.circlesByFilter[filterKey] = sanitizeCircleCollection(nextCircles);
626
+ this.paginationByFilter[filterKey] = nextPagination;
627
+ this.fetchedFilterKeys[filterKey] = true;
628
+ if (this.activeFilterKey !== filterKey) {
629
+ return;
630
+ }
631
+ this.circles = sanitizeCircleCollection(nextCircles);
632
+ this.pagination = { ...nextPagination };
633
+ }
634
+ catch (error) {
635
+ this.error = getErrorMessage(error);
636
+ }
637
+ finally {
638
+ this.loading = false;
639
+ }
640
+ },
641
+ async fetchCircle(id) {
642
+ this.loading = true;
643
+ this.error = null;
644
+ try {
645
+ const circle = await circlesService.get(id);
646
+ this.currentCircle = circle;
647
+ this.applyCircleSnapshot(circle);
648
+ return circle;
649
+ }
650
+ catch (error) {
651
+ this.error = getErrorMessage(error);
652
+ throw error;
653
+ }
654
+ finally {
655
+ this.loading = false;
656
+ }
657
+ },
658
+ async createCircle(data) {
659
+ this.loading = true;
660
+ this.error = null;
661
+ try {
662
+ const result = await circlesService.create(data);
663
+ const newCircle = await circlesService.get(result.id);
664
+ this.applyCircleSnapshot(newCircle);
665
+ this.currentCircle = newCircle;
666
+ return newCircle;
667
+ }
668
+ catch (error) {
669
+ this.error = getErrorMessage(error);
670
+ throw error;
671
+ }
672
+ finally {
673
+ this.loading = false;
674
+ }
675
+ },
676
+ async updateCircle(id, data) {
677
+ this.loading = true;
678
+ this.error = null;
679
+ try {
680
+ const updatedCircle = await circlesService.update(id, data);
681
+ this.applyCircleSnapshot(updatedCircle);
682
+ this.currentCircle = updatedCircle;
683
+ return updatedCircle;
684
+ }
685
+ catch (error) {
686
+ this.error = getErrorMessage(error);
687
+ throw error;
688
+ }
689
+ finally {
690
+ this.loading = false;
691
+ }
692
+ },
693
+ async deleteCircle(id) {
694
+ this.loading = true;
695
+ this.error = null;
696
+ try {
697
+ await circlesService.delete(id);
698
+ this.circles = this.circles.filter(c => c.id !== id);
699
+ if (this.currentCircle?.id === id) {
700
+ this.currentCircle = null;
701
+ }
702
+ delete this.members[id];
703
+ delete this.membersPagination[id];
704
+ delete this.joinRequests[id];
705
+ delete this.joinRequestsPagination[id];
706
+ delete this.managementActors[id];
707
+ delete this.managementSections[id];
708
+ delete this.managementBootstrapLoading[id];
709
+ delete this.managementBootstrapError[id];
710
+ delete this.managementSectionLoading[id];
711
+ delete this.managementSectionErrors[id];
712
+ delete this.managementBootstrappedAt[id];
713
+ delete this.roleDefinitions[id];
714
+ delete this.roleDefinitionsPagination[id];
715
+ delete this.managementCounts[id];
716
+ delete this.managementPermissionCatalog[id];
717
+ delete this.roleAssignments[id];
718
+ delete this.roleAssignmentsPagination[id];
719
+ delete this.reports[id];
720
+ delete this.reportsPagination[id];
721
+ delete this.bans[id];
722
+ delete this.bansPagination[id];
723
+ delete this.mutes[id];
724
+ delete this.mutesPagination[id];
725
+ delete this.auditLog[id];
726
+ delete this.auditLogPagination[id];
727
+ delete this.automodRules[id];
728
+ delete this.automodRulesPagination[id];
729
+ }
730
+ catch (error) {
731
+ this.error = getErrorMessage(error);
732
+ throw error;
733
+ }
734
+ finally {
735
+ this.loading = false;
736
+ }
737
+ },
738
+ async joinCircle(id) {
739
+ this.error = null;
740
+ try {
741
+ await circlesService.join(id);
742
+ const circleIndex = this.circles.findIndex(c => c.id === id);
743
+ if (circleIndex !== -1) {
744
+ const circle = this.circles[circleIndex];
745
+ if (!circle) {
746
+ return;
747
+ }
748
+ this.circles.splice(circleIndex, 1, {
749
+ ...circle,
750
+ is_member: true,
751
+ user_role: 'member',
752
+ member_count: (circle.member_count || 0) + 1,
753
+ can_join: false,
754
+ can_leave: true
755
+ });
756
+ }
757
+ if (this.currentCircle?.id === id) {
758
+ this.currentCircle = {
759
+ ...this.currentCircle,
760
+ is_member: true,
761
+ user_role: 'member',
762
+ member_count: (this.currentCircle.member_count || 0) + 1,
763
+ can_join: false,
764
+ can_leave: true
765
+ };
766
+ }
767
+ }
768
+ catch (error) {
769
+ this.error = getErrorMessage(error);
770
+ throw error;
771
+ }
772
+ },
773
+ async leaveCircle(id) {
774
+ this.error = null;
775
+ try {
776
+ await circlesService.leave(id);
777
+ const circleIndex = this.circles.findIndex(c => c.id === id);
778
+ if (circleIndex !== -1) {
779
+ const circle = this.circles[circleIndex];
780
+ if (!circle) {
781
+ return;
782
+ }
783
+ this.circles.splice(circleIndex, 1, {
784
+ ...circle,
785
+ is_member: false,
786
+ user_role: null,
787
+ member_count: Math.max((circle.member_count || 0) - 1, 0),
788
+ can_join: circle.visibility === 'public',
789
+ can_leave: false,
790
+ can_request_to_join: circle.visibility !== 'public',
791
+ has_pending_request: false
792
+ });
793
+ }
794
+ this.pendingRequestCircleIds = this.pendingRequestCircleIds.filter(cid => cid !== id);
795
+ if (this.currentCircle?.id === id) {
796
+ this.currentCircle = {
797
+ ...this.currentCircle,
798
+ is_member: false,
799
+ user_role: null,
800
+ member_count: Math.max((this.currentCircle.member_count || 0) - 1, 0),
801
+ can_join: this.currentCircle.visibility === 'public',
802
+ can_leave: false,
803
+ can_request_to_join: this.currentCircle.visibility !== 'public',
804
+ has_pending_request: false
805
+ };
806
+ }
807
+ }
808
+ catch (error) {
809
+ this.error = getErrorMessage(error);
810
+ throw error;
811
+ }
812
+ },
813
+ applyLiveMemberCount(circleId, memberCount) {
814
+ const nextCount = Math.max(memberCount, 0);
815
+ const circleIndex = this.circles.findIndex(c => c.id === circleId);
816
+ if (circleIndex !== -1) {
817
+ const circle = this.circles[circleIndex];
818
+ if (circle) {
819
+ this.circles.splice(circleIndex, 1, {
820
+ ...circle,
821
+ member_count: nextCount
822
+ });
823
+ }
824
+ }
825
+ if (this.currentCircle?.id === circleId) {
826
+ this.currentCircle = {
827
+ ...this.currentCircle,
828
+ member_count: nextCount
829
+ };
830
+ }
831
+ },
832
+ async fetchMembers(circleId, refresh = false) {
833
+ if (refresh) {
834
+ this.members[circleId] = [];
835
+ this.membersPagination[circleId] = {
836
+ cursor: null,
837
+ hasMore: true
838
+ };
839
+ }
840
+ this.loading = true;
841
+ this.error = null;
842
+ try {
843
+ const pagination = this.membersPagination[circleId] || { cursor: null, hasMore: true };
844
+ const response = await circlesService.getMembers(circleId, pagination.cursor);
845
+ if (refresh) {
846
+ this.members[circleId] = response.data;
847
+ }
848
+ else {
849
+ this.members[circleId] = [
850
+ ...(this.members[circleId] || []),
851
+ ...response.data
852
+ ];
853
+ }
854
+ this.membersPagination[circleId] = toCursorPaginationState(response);
855
+ this.syncRoleAssignmentCompatibility(circleId);
856
+ }
857
+ catch (error) {
858
+ this.error = getErrorMessage(error);
859
+ throw error;
860
+ }
861
+ finally {
862
+ this.loading = false;
863
+ }
864
+ },
865
+ addMember(circleId, member) {
866
+ if (!this.members[circleId]) {
867
+ this.members[circleId] = [];
868
+ }
869
+ if (!this.members[circleId].find(m => m.id === member.id)) {
870
+ this.members[circleId].push(member);
871
+ this.syncRoleAssignmentCompatibility(circleId);
872
+ const circleIndex = this.circles.findIndex(c => c.id === circleId);
873
+ if (circleIndex !== -1) {
874
+ const circle = this.circles[circleIndex];
875
+ if (circle) {
876
+ circle.member_count = (circle.member_count || 0) + 1;
877
+ }
878
+ }
879
+ if (this.currentCircle?.id === circleId) {
880
+ this.currentCircle.member_count = (this.currentCircle.member_count || 0) + 1;
881
+ }
882
+ }
883
+ },
884
+ removeMember(circleId, userId) {
885
+ if (this.members[circleId]) {
886
+ const nextMembers = this.members[circleId].filter(m => m.user_id !== userId && m.user?.id !== userId);
887
+ this.members[circleId] = nextMembers;
888
+ this.syncRoleAssignmentCompatibility(circleId);
889
+ const nextCount = nextMembers.length;
890
+ const circleIndex = this.circles.findIndex(c => c.id === circleId);
891
+ if (circleIndex !== -1) {
892
+ const circle = this.circles[circleIndex];
893
+ if (circle) {
894
+ circle.member_count = nextCount;
895
+ }
896
+ }
897
+ if (this.currentCircle?.id === circleId) {
898
+ this.currentCircle.member_count = nextCount;
899
+ }
900
+ return;
901
+ }
902
+ const circleIndex = this.circles.findIndex(c => c.id === circleId);
903
+ if (circleIndex !== -1) {
904
+ const circle = this.circles[circleIndex];
905
+ if (circle) {
906
+ circle.member_count = Math.max((circle.member_count || 0) - 1, 0);
907
+ }
908
+ }
909
+ if (this.currentCircle?.id === circleId) {
910
+ this.currentCircle.member_count = Math.max((this.currentCircle.member_count || 0) - 1, 0);
911
+ }
912
+ },
913
+ async changeMemberRole(circleId, userId, newRole) {
914
+ this.error = null;
915
+ try {
916
+ await circlesService.changeMemberRole(circleId, userId, newRole);
917
+ const assignedRoles = newRole === 'admin'
918
+ ? (this.roleDefinitions[circleId] ?? []).filter((role) => role.slug === 'group-admin')
919
+ : [];
920
+ if (newRole === 'admin' && assignedRoles.length === 0) {
921
+ this.updateMemberRoleState(circleId, userId, {
922
+ membership_role: 'member',
923
+ display_role: 'admin',
924
+ role: 'admin',
925
+ assigned_roles: [],
926
+ effective_permissions: [],
927
+ });
928
+ return;
929
+ }
930
+ if (newRole === 'member') {
931
+ this.updateMemberRoleState(circleId, userId, {
932
+ membership_role: 'member',
933
+ display_role: 'member',
934
+ role: 'member',
935
+ assigned_roles: [],
936
+ effective_permissions: [],
937
+ });
938
+ return;
939
+ }
940
+ this.applyMemberRoleAssignments(circleId, userId, assignedRoles, buildEffectivePermissions(assignedRoles));
941
+ }
942
+ catch (error) {
943
+ this.error = getErrorMessage(error);
944
+ throw error;
945
+ }
946
+ },
947
+ async removeMemberFromCircle(circleId, userId) {
948
+ this.error = null;
949
+ try {
950
+ await circlesService.removeMember(circleId, userId);
951
+ this.removeMember(circleId, userId);
952
+ }
953
+ catch (error) {
954
+ this.error = getErrorMessage(error);
955
+ throw error;
956
+ }
957
+ },
958
+ async muteMemberInCircle(circleId, userId, options) {
959
+ this.error = null;
960
+ try {
961
+ await circlesService.muteMember(circleId, userId, options);
962
+ await Promise.allSettled([
963
+ this.fetchMutes(circleId, true),
964
+ this.fetchAuditLog(circleId, true),
965
+ ]);
966
+ }
967
+ catch (error) {
968
+ this.error = getErrorMessage(error);
969
+ throw error;
970
+ }
971
+ },
972
+ async bootstrapManagement(circleId) {
973
+ this.ensureManagementState(circleId);
974
+ this.managementBootstrapLoading[circleId] = true;
975
+ this.managementBootstrapError[circleId] = null;
976
+ try {
977
+ const bootstrap = await circlesService.getManagementBootstrap(circleId);
978
+ this.applyManagementBootstrap(bootstrap);
979
+ return bootstrap;
980
+ }
981
+ catch (error) {
982
+ const message = getErrorMessage(error);
983
+ this.managementBootstrapError[circleId] = message;
984
+ this.error = message;
985
+ throw error;
986
+ }
987
+ finally {
988
+ this.managementBootstrapLoading[circleId] = false;
989
+ }
990
+ },
991
+ async fetchRoleDefinitions(circleId, refresh = false) {
992
+ this.setManagementSectionLoading(circleId, 'roles', true);
993
+ this.setManagementSectionError(circleId, 'roles', null);
994
+ try {
995
+ const roleService = getRoleStoreService(circlesService);
996
+ const cursor = refresh ? null : this.roleDefinitionsPagination[circleId]?.cursor ?? null;
997
+ let response;
998
+ let usedLegacyListRolesFallback = false;
999
+ if (typeof roleService.fetchRoleDefinitions === 'function') {
1000
+ response = await roleService.fetchRoleDefinitions(circleId, cursor);
1001
+ }
1002
+ else if (typeof roleService.listRoleDefinitions === 'function') {
1003
+ response = await roleService.listRoleDefinitions(circleId, cursor);
1004
+ }
1005
+ else if (typeof roleService.listRoles === 'function') {
1006
+ usedLegacyListRolesFallback = true;
1007
+ response = await roleService.listRoles(circleId, cursor);
1008
+ }
1009
+ else {
1010
+ response = [];
1011
+ }
1012
+ const incomingRoles = extractRoleDefinitions(response);
1013
+ if (usedLegacyListRolesFallback && incomingRoles.length === 0) {
1014
+ return this.roleDefinitions[circleId] ?? [];
1015
+ }
1016
+ const nextRoles = refresh
1017
+ ? [...incomingRoles]
1018
+ : [
1019
+ ...(this.roleDefinitions[circleId] ?? []),
1020
+ ...incomingRoles.filter((role) => !(this.roleDefinitions[circleId] ?? []).some((existing) => existing.id === role.id)),
1021
+ ];
1022
+ this.setRoleDefinitionsState(circleId, nextRoles, toUnknownCursorPaginationState(response));
1023
+ return nextRoles;
1024
+ }
1025
+ catch (error) {
1026
+ const message = getErrorMessage(error);
1027
+ this.setManagementSectionError(circleId, 'roles', message);
1028
+ throw error;
1029
+ }
1030
+ finally {
1031
+ this.setManagementSectionLoading(circleId, 'roles', false);
1032
+ }
1033
+ },
1034
+ async createRoleDefinition(circleId, input) {
1035
+ this.error = null;
1036
+ try {
1037
+ const roleService = getRoleStoreService(circlesService);
1038
+ let response;
1039
+ if (typeof roleService.createRoleDefinition === 'function') {
1040
+ response = await roleService.createRoleDefinition(circleId, input);
1041
+ }
1042
+ else if (typeof roleService.createRole === 'function') {
1043
+ response = await roleService.createRole(circleId, input);
1044
+ }
1045
+ else {
1046
+ throw new Error('Role definition creation is unavailable.');
1047
+ }
1048
+ const role = normalizeRoleDefinition(response);
1049
+ if (role === null) {
1050
+ throw new Error('Invalid role definition payload');
1051
+ }
1052
+ this.upsertRoleDefinitionState(circleId, role);
1053
+ await this.fetchRoleDefinitions(circleId, true);
1054
+ return role;
1055
+ }
1056
+ catch (error) {
1057
+ this.error = getErrorMessage(error);
1058
+ throw error;
1059
+ }
1060
+ },
1061
+ async updateRoleDefinition(circleId, roleId, input) {
1062
+ this.error = null;
1063
+ try {
1064
+ const roleService = getRoleStoreService(circlesService);
1065
+ let response;
1066
+ if (typeof roleService.updateRoleDefinition === 'function') {
1067
+ response = await roleService.updateRoleDefinition(circleId, roleId, input);
1068
+ }
1069
+ else if (typeof roleService.updateRole === 'function') {
1070
+ response = await roleService.updateRole(circleId, roleId, input);
1071
+ }
1072
+ else {
1073
+ throw new Error('Role definition updates are unavailable.');
1074
+ }
1075
+ const role = normalizeRoleDefinition(response);
1076
+ if (role === null) {
1077
+ throw new Error('Invalid role definition payload');
1078
+ }
1079
+ this.upsertRoleDefinitionState(circleId, role);
1080
+ await this.fetchRoleDefinitions(circleId, true);
1081
+ return role;
1082
+ }
1083
+ catch (error) {
1084
+ this.error = getErrorMessage(error);
1085
+ throw error;
1086
+ }
1087
+ },
1088
+ async replaceRoleDefinitionPermissions(circleId, roleId, permissions) {
1089
+ this.error = null;
1090
+ try {
1091
+ const roleService = getRoleStoreService(circlesService);
1092
+ const sanitizedPermissions = sanitizePermissionKeys(permissions);
1093
+ let response;
1094
+ if (typeof roleService.replaceRoleDefinitionPermissions === 'function') {
1095
+ response = await roleService.replaceRoleDefinitionPermissions(circleId, roleId, sanitizedPermissions);
1096
+ }
1097
+ else if (typeof roleService.replaceRolePermissions === 'function') {
1098
+ response = await roleService.replaceRolePermissions(circleId, roleId, sanitizedPermissions);
1099
+ }
1100
+ else {
1101
+ throw new Error('Role permission updates are unavailable.');
1102
+ }
1103
+ const role = normalizeRoleDefinition(response);
1104
+ if (role === null) {
1105
+ throw new Error('Invalid role definition payload');
1106
+ }
1107
+ this.upsertRoleDefinitionState(circleId, role);
1108
+ await this.fetchRoleDefinitions(circleId, true);
1109
+ return role;
1110
+ }
1111
+ catch (error) {
1112
+ this.error = getErrorMessage(error);
1113
+ throw error;
1114
+ }
1115
+ },
1116
+ async archiveRoleDefinition(circleId, roleId) {
1117
+ this.error = null;
1118
+ try {
1119
+ const roleService = getRoleStoreService(circlesService);
1120
+ if (typeof roleService.archiveRoleDefinition === 'function') {
1121
+ await roleService.archiveRoleDefinition(circleId, roleId);
1122
+ }
1123
+ else if (typeof roleService.archiveRole === 'function') {
1124
+ await roleService.archiveRole(circleId, roleId);
1125
+ }
1126
+ else {
1127
+ throw new Error('Role archiving is unavailable.');
1128
+ }
1129
+ this.removeRoleDefinitionState(circleId, roleId);
1130
+ }
1131
+ catch (error) {
1132
+ this.error = getErrorMessage(error);
1133
+ throw error;
1134
+ }
1135
+ },
1136
+ async assignMemberRoles(circleId, userId, roleIds) {
1137
+ this.error = null;
1138
+ try {
1139
+ const roleService = getRoleStoreService(circlesService);
1140
+ const normalizedRoleIds = [...new Set(roleIds.filter((roleId) => roleId.trim().length > 0))];
1141
+ if (typeof roleService.assignMemberRoles === 'function') {
1142
+ const response = await roleService.assignMemberRoles(circleId, userId, normalizedRoleIds);
1143
+ const responseRecord = isRecord(response) ? response : {};
1144
+ const assignedRoles = sanitizeRoleDefinitions(responseRecord.assigned_roles);
1145
+ const effectivePermissions = sanitizePermissionKeys(responseRecord.effective_permissions);
1146
+ this.applyMemberRoleAssignments(circleId, userId, assignedRoles, effectivePermissions);
1147
+ return {
1148
+ assigned_roles: assignedRoles,
1149
+ effective_permissions: effectivePermissions,
1150
+ };
1151
+ }
1152
+ const selectedRoles = (this.roleDefinitions[circleId] ?? []).filter((role) => normalizedRoleIds.includes(role.id));
1153
+ if (selectedRoles.length === 1 && selectedRoles[0]?.slug === 'group-admin') {
1154
+ await this.changeMemberRole(circleId, userId, 'admin');
1155
+ return {
1156
+ assigned_roles: selectedRoles,
1157
+ effective_permissions: buildEffectivePermissions(selectedRoles),
1158
+ };
1159
+ }
1160
+ throw new Error('Member role assignment is unavailable.');
1161
+ }
1162
+ catch (error) {
1163
+ this.error = getErrorMessage(error);
1164
+ throw error;
1165
+ }
1166
+ },
1167
+ async removeMemberRole(circleId, userId, roleId) {
1168
+ this.error = null;
1169
+ try {
1170
+ const roleService = getRoleStoreService(circlesService);
1171
+ if (typeof roleService.removeMemberRole === 'function') {
1172
+ await roleService.removeMemberRole(circleId, userId, roleId);
1173
+ }
1174
+ else {
1175
+ const member = (this.members[circleId] ?? []).find((candidate) => candidate.user_id === userId || candidate.user?.id === userId);
1176
+ const assignedRoles = member ? extractAssignedRoles(member) : [];
1177
+ const nextAssignedRoles = assignedRoles.filter((role) => role.id !== roleId);
1178
+ const removedRole = assignedRoles.find((role) => role.id === roleId);
1179
+ if (removedRole?.slug === 'group-admin' && nextAssignedRoles.length === 0) {
1180
+ await this.changeMemberRole(circleId, userId, 'member');
1181
+ return;
1182
+ }
1183
+ throw new Error('Member role removal is unavailable.');
1184
+ }
1185
+ const member = (this.members[circleId] ?? []).find((candidate) => candidate.user_id === userId || candidate.user?.id === userId);
1186
+ const nextAssignedRoles = member
1187
+ ? extractAssignedRoles(member).filter((role) => role.id !== roleId)
1188
+ : [];
1189
+ if (nextAssignedRoles.length === 0) {
1190
+ this.updateMemberRoleState(circleId, userId, {
1191
+ membership_role: 'member',
1192
+ display_role: 'member',
1193
+ role: 'member',
1194
+ assigned_roles: [],
1195
+ effective_permissions: [],
1196
+ });
1197
+ return;
1198
+ }
1199
+ this.applyMemberRoleAssignments(circleId, userId, nextAssignedRoles, buildEffectivePermissions(nextAssignedRoles));
1200
+ }
1201
+ catch (error) {
1202
+ this.error = getErrorMessage(error);
1203
+ throw error;
1204
+ }
1205
+ },
1206
+ async fetchRoleAssignments(circleId, refresh = false) {
1207
+ this.setManagementSectionLoading(circleId, 'roles', true);
1208
+ this.setManagementSectionError(circleId, 'roles', null);
1209
+ try {
1210
+ if (refresh || !this.members[circleId]) {
1211
+ await this.fetchMembers(circleId, refresh);
1212
+ }
1213
+ this.syncRoleAssignmentCompatibility(circleId);
1214
+ return this.roleAssignments[circleId] ?? [];
1215
+ }
1216
+ catch (error) {
1217
+ const message = getErrorMessage(error);
1218
+ this.setManagementSectionError(circleId, 'roles', message);
1219
+ throw error;
1220
+ }
1221
+ finally {
1222
+ this.setManagementSectionLoading(circleId, 'roles', false);
1223
+ }
1224
+ },
1225
+ async transferOwnership(circleId, userId) {
1226
+ this.error = null;
1227
+ try {
1228
+ await circlesService.transferOwnership(circleId, userId);
1229
+ await this.bootstrapManagement(circleId);
1230
+ }
1231
+ catch (error) {
1232
+ this.error = getErrorMessage(error);
1233
+ throw error;
1234
+ }
1235
+ },
1236
+ async fetchModerationReports(circleId, refresh = false, status = 'pending') {
1237
+ this.setManagementSectionLoading(circleId, 'reports', true);
1238
+ this.setManagementSectionError(circleId, 'reports', null);
1239
+ try {
1240
+ const cursor = refresh ? null : this.reportsPagination[circleId]?.cursor ?? null;
1241
+ const response = await circlesService.listModerationReports(circleId, cursor, status);
1242
+ const nextReports = refresh
1243
+ ? [...response.data]
1244
+ : [
1245
+ ...(this.reports[circleId] ?? []),
1246
+ ...response.data.filter((report) => !(this.reports[circleId] ?? []).some((existing) => existing.id === report.id)),
1247
+ ];
1248
+ this.reports[circleId] = nextReports;
1249
+ this.reportsPagination[circleId] = toCursorPaginationState(response);
1250
+ return nextReports;
1251
+ }
1252
+ catch (error) {
1253
+ const message = getErrorMessage(error);
1254
+ this.setManagementSectionError(circleId, 'reports', message);
1255
+ throw error;
1256
+ }
1257
+ finally {
1258
+ this.setManagementSectionLoading(circleId, 'reports', false);
1259
+ }
1260
+ },
1261
+ async reportMemberInCircle(circleId, subjectUserId, category, notes) {
1262
+ this.error = null;
1263
+ try {
1264
+ await circlesService.reportMember(circleId, subjectUserId, category, notes);
1265
+ await Promise.allSettled([
1266
+ this.fetchModerationReports(circleId, true),
1267
+ this.fetchAuditLog(circleId, true),
1268
+ ]);
1269
+ }
1270
+ catch (error) {
1271
+ this.error = getErrorMessage(error);
1272
+ throw error;
1273
+ }
1274
+ },
1275
+ async resolveReportInCircle(circleId, reportId, decision, resolutionNotes) {
1276
+ this.error = null;
1277
+ try {
1278
+ const result = await circlesService.resolveModerationReport(circleId, reportId, decision, resolutionNotes);
1279
+ if (this.reports[circleId]) {
1280
+ this.reports[circleId] = this.reports[circleId].map((report) => (report.id === reportId
1281
+ ? {
1282
+ ...report,
1283
+ status: result.status === 'actioned' || result.status === 'dismissed'
1284
+ ? result.status
1285
+ : report.status,
1286
+ resolution_notes: resolutionNotes ?? report.resolution_notes ?? null,
1287
+ }
1288
+ : report));
1289
+ }
1290
+ await this.fetchAuditLog(circleId, true);
1291
+ return result;
1292
+ }
1293
+ catch (error) {
1294
+ this.error = getErrorMessage(error);
1295
+ throw error;
1296
+ }
1297
+ },
1298
+ async fetchBans(circleId, refresh = false) {
1299
+ this.setManagementSectionLoading(circleId, 'bans', true);
1300
+ this.setManagementSectionError(circleId, 'bans', null);
1301
+ try {
1302
+ const cursor = refresh ? null : this.bansPagination[circleId]?.cursor ?? null;
1303
+ const response = await circlesService.listBans(circleId, cursor);
1304
+ const nextBans = refresh
1305
+ ? [...response.data]
1306
+ : [
1307
+ ...(this.bans[circleId] ?? []),
1308
+ ...response.data.filter((ban) => !(this.bans[circleId] ?? []).some((existing) => existing.id === ban.id)),
1309
+ ];
1310
+ this.bans[circleId] = nextBans;
1311
+ this.bansPagination[circleId] = toCursorPaginationState(response);
1312
+ return nextBans;
1313
+ }
1314
+ catch (error) {
1315
+ const message = getErrorMessage(error);
1316
+ this.setManagementSectionError(circleId, 'bans', message);
1317
+ throw error;
1318
+ }
1319
+ finally {
1320
+ this.setManagementSectionLoading(circleId, 'bans', false);
1321
+ }
1322
+ },
1323
+ async banMemberInCircle(circleId, userId, options) {
1324
+ this.error = null;
1325
+ try {
1326
+ await circlesService.banMember(circleId, userId, options);
1327
+ this.removeMember(circleId, userId);
1328
+ await Promise.allSettled([
1329
+ this.fetchBans(circleId, true),
1330
+ this.fetchAuditLog(circleId, true),
1331
+ this.fetchRoleAssignments(circleId, true),
1332
+ ]);
1333
+ }
1334
+ catch (error) {
1335
+ this.error = getErrorMessage(error);
1336
+ throw error;
1337
+ }
1338
+ },
1339
+ async unbanMemberInCircle(circleId, userId) {
1340
+ this.error = null;
1341
+ try {
1342
+ await circlesService.unbanMember(circleId, userId);
1343
+ await Promise.allSettled([
1344
+ this.fetchBans(circleId, true),
1345
+ this.fetchAuditLog(circleId, true),
1346
+ ]);
1347
+ }
1348
+ catch (error) {
1349
+ this.error = getErrorMessage(error);
1350
+ throw error;
1351
+ }
1352
+ },
1353
+ async fetchMutes(circleId, refresh = false) {
1354
+ this.setManagementSectionLoading(circleId, 'mutes', true);
1355
+ this.setManagementSectionError(circleId, 'mutes', null);
1356
+ try {
1357
+ const cursor = refresh ? null : this.mutesPagination[circleId]?.cursor ?? null;
1358
+ const response = await circlesService.listMutes(circleId, cursor);
1359
+ const nextMutes = refresh
1360
+ ? [...response.data]
1361
+ : [
1362
+ ...(this.mutes[circleId] ?? []),
1363
+ ...response.data.filter((mute) => !(this.mutes[circleId] ?? []).some((existing) => existing.id === mute.id)),
1364
+ ];
1365
+ this.mutes[circleId] = nextMutes;
1366
+ this.mutesPagination[circleId] = toCursorPaginationState(response);
1367
+ return nextMutes;
1368
+ }
1369
+ catch (error) {
1370
+ const message = getErrorMessage(error);
1371
+ this.setManagementSectionError(circleId, 'mutes', message);
1372
+ throw error;
1373
+ }
1374
+ finally {
1375
+ this.setManagementSectionLoading(circleId, 'mutes', false);
1376
+ }
1377
+ },
1378
+ async unmuteMemberInCircle(circleId, userId) {
1379
+ this.error = null;
1380
+ try {
1381
+ await circlesService.unmuteMember(circleId, userId);
1382
+ await Promise.allSettled([
1383
+ this.fetchMutes(circleId, true),
1384
+ this.fetchAuditLog(circleId, true),
1385
+ ]);
1386
+ }
1387
+ catch (error) {
1388
+ this.error = getErrorMessage(error);
1389
+ throw error;
1390
+ }
1391
+ },
1392
+ async fetchAuditLog(circleId, refresh = false) {
1393
+ this.setManagementSectionLoading(circleId, 'audit', true);
1394
+ this.setManagementSectionError(circleId, 'audit', null);
1395
+ try {
1396
+ const cursor = refresh ? null : this.auditLogPagination[circleId]?.cursor ?? null;
1397
+ const response = await circlesService.getModerationAuditLog(circleId, cursor);
1398
+ const nextEntries = refresh
1399
+ ? [...response.data]
1400
+ : [
1401
+ ...(this.auditLog[circleId] ?? []),
1402
+ ...response.data.filter((entry) => !(this.auditLog[circleId] ?? []).some((existing) => existing.id === entry.id)),
1403
+ ];
1404
+ this.auditLog[circleId] = nextEntries;
1405
+ this.auditLogPagination[circleId] = toCursorPaginationState(response);
1406
+ return nextEntries;
1407
+ }
1408
+ catch (error) {
1409
+ const message = getErrorMessage(error);
1410
+ this.setManagementSectionError(circleId, 'audit', message);
1411
+ throw error;
1412
+ }
1413
+ finally {
1414
+ this.setManagementSectionLoading(circleId, 'audit', false);
1415
+ }
1416
+ },
1417
+ async fetchAutomodRules(circleId, refresh = false) {
1418
+ this.setManagementSectionLoading(circleId, 'automod', true);
1419
+ this.setManagementSectionError(circleId, 'automod', null);
1420
+ try {
1421
+ const cursor = refresh ? null : this.automodRulesPagination[circleId]?.cursor ?? null;
1422
+ const response = await circlesService.listAutomodRules(circleId, cursor);
1423
+ const nextRules = refresh
1424
+ ? [...response.data]
1425
+ : [
1426
+ ...(this.automodRules[circleId] ?? []),
1427
+ ...response.data.filter((rule) => !(this.automodRules[circleId] ?? []).some((existing) => existing.id === rule.id)),
1428
+ ];
1429
+ this.automodRules[circleId] = nextRules;
1430
+ this.automodRulesPagination[circleId] = toCursorPaginationState(response);
1431
+ return nextRules;
1432
+ }
1433
+ catch (error) {
1434
+ const message = getErrorMessage(error);
1435
+ this.setManagementSectionError(circleId, 'automod', message);
1436
+ throw error;
1437
+ }
1438
+ finally {
1439
+ this.setManagementSectionLoading(circleId, 'automod', false);
1440
+ }
1441
+ },
1442
+ async createAutomodRuleInCircle(circleId, input) {
1443
+ this.error = null;
1444
+ try {
1445
+ const rule = await circlesService.createAutomodRule(circleId, input);
1446
+ this.automodRules[circleId] = [rule, ...(this.automodRules[circleId] ?? [])];
1447
+ await this.fetchAuditLog(circleId, true);
1448
+ return rule;
1449
+ }
1450
+ catch (error) {
1451
+ this.error = getErrorMessage(error);
1452
+ throw error;
1453
+ }
1454
+ },
1455
+ async updateAutomodRuleInCircle(circleId, ruleId, input) {
1456
+ this.error = null;
1457
+ try {
1458
+ const updatedRule = await circlesService.updateAutomodRule(circleId, ruleId, input);
1459
+ this.automodRules[circleId] = (this.automodRules[circleId] ?? []).map((rule) => (rule.id === updatedRule.id ? updatedRule : rule));
1460
+ await this.fetchAuditLog(circleId, true);
1461
+ return updatedRule;
1462
+ }
1463
+ catch (error) {
1464
+ this.error = getErrorMessage(error);
1465
+ throw error;
1466
+ }
1467
+ },
1468
+ async deleteAutomodRuleInCircle(circleId, ruleId) {
1469
+ this.error = null;
1470
+ try {
1471
+ await circlesService.deleteAutomodRule(circleId, ruleId);
1472
+ this.automodRules[circleId] = (this.automodRules[circleId] ?? []).filter((rule) => rule.id !== ruleId);
1473
+ await this.fetchAuditLog(circleId, true);
1474
+ }
1475
+ catch (error) {
1476
+ this.error = getErrorMessage(error);
1477
+ throw error;
1478
+ }
1479
+ },
1480
+ async requestJoinCircle(id, message) {
1481
+ this.error = null;
1482
+ try {
1483
+ await circlesService.requestToJoin(id, message);
1484
+ if (!this.pendingRequestCircleIds.includes(id)) {
1485
+ this.pendingRequestCircleIds.push(id);
1486
+ }
1487
+ const circleIndex = this.circles.findIndex(c => c.id === id);
1488
+ if (circleIndex !== -1) {
1489
+ const circle = this.circles[circleIndex];
1490
+ if (!circle) {
1491
+ return;
1492
+ }
1493
+ this.circles.splice(circleIndex, 1, {
1494
+ ...circle,
1495
+ has_pending_request: true,
1496
+ can_request_to_join: false
1497
+ });
1498
+ }
1499
+ if (this.currentCircle?.id === id) {
1500
+ this.currentCircle = {
1501
+ ...this.currentCircle,
1502
+ has_pending_request: true,
1503
+ can_request_to_join: false
1504
+ };
1505
+ }
1506
+ }
1507
+ catch (error) {
1508
+ this.error = getErrorMessage(error);
1509
+ throw error;
1510
+ }
1511
+ },
1512
+ async fetchJoinRequests(circleId, status) {
1513
+ this.error = null;
1514
+ this.setManagementSectionLoading(circleId, 'requests', true);
1515
+ this.setManagementSectionError(circleId, 'requests', null);
1516
+ try {
1517
+ const response = await circlesService.listJoinRequests(circleId, null, status);
1518
+ this.joinRequests[circleId] = response.data;
1519
+ this.joinRequestsPagination[circleId] = toCursorPaginationState(response);
1520
+ }
1521
+ catch (error) {
1522
+ const message = getErrorMessage(error);
1523
+ this.error = message;
1524
+ this.setManagementSectionError(circleId, 'requests', message);
1525
+ throw error;
1526
+ }
1527
+ finally {
1528
+ this.setManagementSectionLoading(circleId, 'requests', false);
1529
+ }
1530
+ },
1531
+ async moderateJoinRequest(circleId, requestId, decision) {
1532
+ this.error = null;
1533
+ try {
1534
+ await circlesService.moderateJoinRequest(circleId, requestId, decision);
1535
+ if (this.joinRequests[circleId]) {
1536
+ this.joinRequests[circleId] = this.joinRequests[circleId].filter(r => r.id !== requestId);
1537
+ }
1538
+ if (decision === 'approve') {
1539
+ const circleIndex = this.circles.findIndex(c => c.id === circleId);
1540
+ if (circleIndex !== -1) {
1541
+ const circle = this.circles[circleIndex];
1542
+ if (!circle) {
1543
+ return;
1544
+ }
1545
+ this.circles.splice(circleIndex, 1, {
1546
+ ...circle,
1547
+ member_count: (circle.member_count || 0) + 1
1548
+ });
1549
+ }
1550
+ if (this.currentCircle?.id === circleId) {
1551
+ this.currentCircle = {
1552
+ ...this.currentCircle,
1553
+ member_count: (this.currentCircle.member_count || 0) + 1
1554
+ };
1555
+ }
1556
+ }
1557
+ }
1558
+ catch (error) {
1559
+ this.error = getErrorMessage(error);
1560
+ throw error;
1561
+ }
1562
+ },
1563
+ async syncPendingJoinRequestStatuses() {
1564
+ const circleIds = new Set(this.pendingRequestCircleIds);
1565
+ for (const circle of this.circles) {
1566
+ if (circle.has_pending_request) {
1567
+ circleIds.add(circle.id);
1568
+ }
1569
+ }
1570
+ if (circleIds.size === 0) {
1571
+ return;
1572
+ }
1573
+ for (const circleId of circleIds) {
1574
+ try {
1575
+ const latestCircle = await circlesService.get(circleId);
1576
+ const circleIndex = this.circles.findIndex(circle => circle.id === circleId);
1577
+ if (circleIndex !== -1) {
1578
+ const existingCircle = this.circles[circleIndex];
1579
+ if (!existingCircle) {
1580
+ continue;
1581
+ }
1582
+ this.circles.splice(circleIndex, 1, {
1583
+ ...existingCircle,
1584
+ ...latestCircle
1585
+ });
1586
+ }
1587
+ if (this.currentCircle?.id === circleId) {
1588
+ this.currentCircle = {
1589
+ ...this.currentCircle,
1590
+ ...latestCircle
1591
+ };
1592
+ }
1593
+ if (latestCircle.has_pending_request) {
1594
+ if (!this.pendingRequestCircleIds.includes(circleId)) {
1595
+ this.pendingRequestCircleIds.push(circleId);
1596
+ }
1597
+ continue;
1598
+ }
1599
+ this.pendingRequestCircleIds = this.pendingRequestCircleIds.filter(id => id !== circleId);
1600
+ }
1601
+ catch {
1602
+ // Best-effort reconciliation: ignore per-circle failures and retry next poll cycle.
1603
+ }
1604
+ }
1605
+ },
1606
+ async createInviteLink(circleId, options) {
1607
+ this.error = null;
1608
+ try {
1609
+ return await circlesService.createInviteLink(circleId, options);
1610
+ }
1611
+ catch (error) {
1612
+ this.error = getErrorMessage(error);
1613
+ throw error;
1614
+ }
1615
+ },
1616
+ hasPendingRequest(circleId) {
1617
+ return this.pendingRequestCircleIds.includes(circleId);
1618
+ },
1619
+ clearState() {
1620
+ this.circles = [];
1621
+ this.currentCircle = null;
1622
+ this.members = {};
1623
+ this.joinRequests = {};
1624
+ this.joinRequestsPagination = {};
1625
+ this.managementActors = {};
1626
+ this.managementSections = {};
1627
+ this.managementBootstrapLoading = {};
1628
+ this.managementBootstrapError = {};
1629
+ this.managementSectionLoading = {};
1630
+ this.managementSectionErrors = {};
1631
+ this.managementBootstrappedAt = {};
1632
+ this.roleDefinitions = {};
1633
+ this.roleDefinitionsPagination = {};
1634
+ this.managementCounts = {};
1635
+ this.managementPermissionCatalog = {};
1636
+ this.roleAssignments = {};
1637
+ this.roleAssignmentsPagination = {};
1638
+ this.reports = {};
1639
+ this.reportsPagination = {};
1640
+ this.bans = {};
1641
+ this.bansPagination = {};
1642
+ this.mutes = {};
1643
+ this.mutesPagination = {};
1644
+ this.auditLog = {};
1645
+ this.auditLogPagination = {};
1646
+ this.automodRules = {};
1647
+ this.automodRulesPagination = {};
1648
+ this.pendingRequestCircleIds = [];
1649
+ this.loading = false;
1650
+ this.error = null;
1651
+ this.pagination = {
1652
+ cursor: null,
1653
+ hasMore: true
1654
+ };
1655
+ this.activeFilterKey = DEFAULT_FILTER_KEY;
1656
+ this.circlesByFilter = {
1657
+ [DEFAULT_FILTER_KEY]: []
1658
+ };
1659
+ this.paginationByFilter = {
1660
+ [DEFAULT_FILTER_KEY]: defaultPaginationState()
1661
+ };
1662
+ this.fetchedFilterKeys = {};
1663
+ this.membersPagination = {};
1664
+ this.filters = {};
1665
+ }
1666
+ },
1667
+ ...(persist ? { persist } : {})
1668
+ });
1669
+ }
1670
+ //# sourceMappingURL=circles.js.map