@codingfactory/socialkit-vue 0.7.23 → 0.7.24

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.
Files changed (37) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/services/circles.d.ts +7 -0
  5. package/dist/services/circles.d.ts.map +1 -1
  6. package/dist/services/circles.js +34 -5
  7. package/dist/services/circles.js.map +1 -1
  8. package/dist/stores/__tests__/discussion.spec.d.ts +2 -0
  9. package/dist/stores/__tests__/discussion.spec.d.ts.map +1 -0
  10. package/dist/stores/__tests__/discussion.spec.js +768 -0
  11. package/dist/stores/__tests__/discussion.spec.js.map +1 -0
  12. package/dist/stores/circles.d.ts +144 -0
  13. package/dist/stores/circles.d.ts.map +1 -1
  14. package/dist/stores/circles.js +7 -1
  15. package/dist/stores/circles.js.map +1 -1
  16. package/dist/stores/content.d.ts.map +1 -1
  17. package/dist/stores/content.js +6 -3
  18. package/dist/stores/content.js.map +1 -1
  19. package/dist/stores/discussion.d.ts +714 -15
  20. package/dist/stores/discussion.d.ts.map +1 -1
  21. package/dist/stores/discussion.js +272 -65
  22. package/dist/stores/discussion.js.map +1 -1
  23. package/dist/types/content.d.ts +3 -2
  24. package/dist/types/content.d.ts.map +1 -1
  25. package/dist/types/content.js +2 -1
  26. package/dist/types/content.js.map +1 -1
  27. package/dist/types/discussion.d.ts +38 -0
  28. package/dist/types/discussion.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/index.ts +4 -0
  31. package/src/services/circles.ts +45 -5
  32. package/src/stores/__tests__/discussion.spec.ts +945 -0
  33. package/src/stores/circles.ts +7 -1
  34. package/src/stores/content.ts +6 -3
  35. package/src/stores/discussion.ts +333 -76
  36. package/src/types/content.ts +3 -2
  37. package/src/types/discussion.ts +43 -0
@@ -124,6 +124,8 @@ export function createDiscussionStoreDefinition(config) {
124
124
  const spaceMemberships = ref({});
125
125
  const spaceMembershipLoading = ref({});
126
126
  const threads = ref([]);
127
+ const browseThreadList = ref([]);
128
+ const currentBrowseMode = ref(null);
127
129
  const currentThread = ref(null);
128
130
  const replies = ref([]);
129
131
  const currentReplySort = ref('best');
@@ -131,8 +133,10 @@ export function createDiscussionStoreDefinition(config) {
131
133
  const threadsLoadingState = createLoadingState();
132
134
  const repliesLoadingState = createLoadingState();
133
135
  const loading = ref(false);
136
+ let activeLoadingRequests = 0;
134
137
  const error = ref(null);
135
138
  const nextCursor = ref(null);
139
+ const browseNextCursor = ref(null);
136
140
  const repliesNextCursor = ref(null);
137
141
  const latestSpaceSlug = ref(null);
138
142
  const latestThreadId = ref(null);
@@ -237,6 +241,17 @@ export function createDiscussionStoreDefinition(config) {
237
241
  }
238
242
  return hasQuotedReplyBody(incomingQuotedReply) ? incomingQuotedReply : null;
239
243
  }
244
+ /**
245
+ * Check whether a reply-like object carries a truthy `is_author` flag.
246
+ * The field is not part of the Reply TS interface (it is user-specific
247
+ * metadata injected by the API) so we access it reflectively.
248
+ */
249
+ function hasReplyAuthorFlag(reply) {
250
+ return Reflect.get(reply, 'is_author') === true;
251
+ }
252
+ function setReplyAuthorFlag(reply, value) {
253
+ Reflect.set(reply, 'is_author', value);
254
+ }
240
255
  function normalizeReplyPayload(reply, existingReply) {
241
256
  const normalized = { ...reply };
242
257
  const hasQuotedReplyId = typeof normalized.quoted_reply_id === 'string'
@@ -448,6 +463,14 @@ export function createDiscussionStoreDefinition(config) {
448
463
  THREAD: 'thread',
449
464
  REPLY: 'reply',
450
465
  };
466
+ function beginLoading() {
467
+ activeLoadingRequests += 1;
468
+ loading.value = true;
469
+ }
470
+ function endLoading() {
471
+ activeLoadingRequests = Math.max(0, activeLoadingRequests - 1);
472
+ loading.value = activeLoadingRequests > 0;
473
+ }
451
474
  function flattenTree(tree) {
452
475
  const result = [];
453
476
  function walk(nodes) {
@@ -467,12 +490,30 @@ export function createDiscussionStoreDefinition(config) {
467
490
  function leafSpaces() {
468
491
  return spaces.value.filter((space) => space.is_leaf !== false);
469
492
  }
470
- async function loadSpaces() {
493
+ function buildSpaceQueryString(options) {
494
+ const params = new URLSearchParams();
495
+ params.set('tree', options?.tree === false ? '0' : '1');
496
+ if (options?.kind) {
497
+ params.set('kind', options.kind);
498
+ }
499
+ if (typeof options?.scope_type === 'string' && options.scope_type.trim().length > 0) {
500
+ params.set('scope_type', options.scope_type.trim());
501
+ }
502
+ if (typeof options?.scope_id === 'string' && options.scope_id.trim().length > 0) {
503
+ params.set('scope_id', options.scope_id.trim());
504
+ }
505
+ if (typeof options?.parent_id === 'string' && options.parent_id.trim().length > 0) {
506
+ params.set('parent_id', options.parent_id.trim());
507
+ }
508
+ return params.toString();
509
+ }
510
+ async function loadSpaces(options) {
471
511
  spacesLoadingState.setLoading(true);
472
- loading.value = true;
512
+ beginLoading();
473
513
  error.value = null;
474
514
  try {
475
- const response = await client.get('/v1/discussion/spaces?tree=1');
515
+ const queryString = buildSpaceQueryString(options);
516
+ const response = await client.get(`/v1/discussion/spaces?${queryString}`);
476
517
  const responseData = toRecord(response.data);
477
518
  const treeData = Array.isArray(responseData?.items)
478
519
  ? responseData.items
@@ -481,9 +522,7 @@ export function createDiscussionStoreDefinition(config) {
481
522
  : [];
482
523
  spaceTree.value = treeData;
483
524
  spaces.value = flattenTree(treeData);
484
- if (treeData.length === 0) {
485
- spacesLoadingState.setEmpty(true);
486
- }
525
+ spacesLoadingState.setEmpty(treeData.length === 0);
487
526
  return response.data;
488
527
  }
489
528
  catch (err) {
@@ -495,7 +534,64 @@ export function createDiscussionStoreDefinition(config) {
495
534
  }
496
535
  finally {
497
536
  spacesLoadingState.setLoading(false);
498
- loading.value = false;
537
+ endLoading();
538
+ }
539
+ }
540
+ async function createSpace(input) {
541
+ beginLoading();
542
+ error.value = null;
543
+ try {
544
+ const response = await client.post('/v1/discussion/spaces', {
545
+ ...input,
546
+ ...(typeof input.parent_id === 'string' ? { parent_id: input.parent_id } : { parent_id: null }),
547
+ ...(typeof input.description === 'string' && input.description.trim().length > 0
548
+ ? { description: input.description.trim() }
549
+ : {}),
550
+ ...(input.meta ? { meta: input.meta } : {}),
551
+ });
552
+ return response.data;
553
+ }
554
+ catch (err) {
555
+ logger.error('Failed to create discussion space:', err);
556
+ error.value = getErrorMessage(err, 'Failed to create discussion space');
557
+ throw err;
558
+ }
559
+ finally {
560
+ endLoading();
561
+ }
562
+ }
563
+ async function moveSpace(spaceId, parentId) {
564
+ beginLoading();
565
+ error.value = null;
566
+ try {
567
+ const response = await client.post(`/v1/discussion/spaces/${spaceId}/move`, {
568
+ parent_id: parentId ?? null,
569
+ });
570
+ return response.data;
571
+ }
572
+ catch (err) {
573
+ logger.error('Failed to move discussion space:', err);
574
+ error.value = getErrorMessage(err, 'Failed to move discussion space');
575
+ throw err;
576
+ }
577
+ finally {
578
+ endLoading();
579
+ }
580
+ }
581
+ async function reorderSpaces(ids) {
582
+ beginLoading();
583
+ error.value = null;
584
+ try {
585
+ const response = await client.post('/v1/discussion/spaces/reorder', { ids });
586
+ return response.data;
587
+ }
588
+ catch (err) {
589
+ logger.error('Failed to reorder discussion spaces:', err);
590
+ error.value = getErrorMessage(err, 'Failed to reorder discussion spaces');
591
+ throw err;
592
+ }
593
+ finally {
594
+ endLoading();
499
595
  }
500
596
  }
501
597
  async function loadSpaceDetail(slug, options) {
@@ -577,9 +673,8 @@ export function createDiscussionStoreDefinition(config) {
577
673
  latestSpaceSlug.value = spaceSlug;
578
674
  }
579
675
  threadsLoadingState.setLoading(true);
580
- loading.value = true;
676
+ beginLoading();
581
677
  error.value = null;
582
- let isStale = false;
583
678
  try {
584
679
  const queryParams = new URLSearchParams();
585
680
  if (cursor) {
@@ -588,6 +683,9 @@ export function createDiscussionStoreDefinition(config) {
588
683
  if (options?.sort) {
589
684
  queryParams.set('sort', options.sort);
590
685
  }
686
+ if (options?.filter && options.filter !== 'all') {
687
+ queryParams.set('filter', options.filter);
688
+ }
591
689
  const queryString = queryParams.toString();
592
690
  const url = `/v1/discussion/spaces/${spaceSlug}/threads${queryString.length > 0 ? `?${queryString}` : ''}`;
593
691
  const requestConfig = {
@@ -604,7 +702,6 @@ export function createDiscussionStoreDefinition(config) {
604
702
  response = await client.get(url, requestConfig);
605
703
  }
606
704
  if (latestSpaceSlug.value !== spaceSlug) {
607
- isStale = true;
608
705
  return response.data;
609
706
  }
610
707
  const newThreads = getThreadsFromPayload(response.data).map((thread) => normalizeThreadReplyCount(thread));
@@ -630,7 +727,6 @@ export function createDiscussionStoreDefinition(config) {
630
727
  }
631
728
  catch (err) {
632
729
  if (options?.signal?.aborted || latestSpaceSlug.value !== spaceSlug) {
633
- isStale = true;
634
730
  return undefined;
635
731
  }
636
732
  if (isAxiosError(err) && err.response?.status === 404) {
@@ -654,10 +750,67 @@ export function createDiscussionStoreDefinition(config) {
654
750
  throw err;
655
751
  }
656
752
  finally {
657
- if (!isStale) {
658
- threadsLoadingState.setLoading(false);
659
- loading.value = false;
753
+ threadsLoadingState.setLoading(false);
754
+ endLoading();
755
+ }
756
+ }
757
+ async function browseThreads(view, cursor, options) {
758
+ if (!cursor || currentBrowseMode.value !== view) {
759
+ currentBrowseMode.value = view;
760
+ }
761
+ threadsLoadingState.setLoading(true);
762
+ beginLoading();
763
+ error.value = null;
764
+ try {
765
+ const queryParams = new URLSearchParams();
766
+ if (cursor) {
767
+ queryParams.set('cursor', cursor);
660
768
  }
769
+ const queryString = queryParams.toString();
770
+ const url = `/v1/discussion/threads/${view}${queryString.length > 0 ? `?${queryString}` : ''}`;
771
+ const requestConfig = {
772
+ ...(options?.signal ? { signal: options.signal } : {}),
773
+ };
774
+ let response;
775
+ try {
776
+ response = await client.get(url, requestConfig);
777
+ }
778
+ catch (requestError) {
779
+ if (!isTransientThreadListError(requestError) || options?.signal?.aborted) {
780
+ throw requestError;
781
+ }
782
+ response = await client.get(url, requestConfig);
783
+ }
784
+ if (currentBrowseMode.value !== view) {
785
+ return response.data;
786
+ }
787
+ const newThreads = getThreadsFromPayload(response.data).map((thread) => normalizeThreadReplyCount(thread));
788
+ if (cursor) {
789
+ browseThreadList.value = mergeUniqueById(browseThreadList.value, newThreads);
790
+ }
791
+ else {
792
+ browseThreadList.value = newThreads;
793
+ if (newThreads.length === 0) {
794
+ threadsLoadingState.setEmpty(true);
795
+ }
796
+ }
797
+ browseNextCursor.value = getThreadNextCursorFromPayload(response.data);
798
+ currentSpace.value = null;
799
+ return response.data;
800
+ }
801
+ catch (err) {
802
+ if (options?.signal?.aborted || currentBrowseMode.value !== view) {
803
+ return undefined;
804
+ }
805
+ logger.error('Failed to browse discussion threads:', err);
806
+ const errorMessage = getErrorMessage(err, 'Failed to load discussion threads');
807
+ threadsLoadingState.setError(new Error(errorMessage));
808
+ error.value = errorMessage;
809
+ throw err;
810
+ }
811
+ finally {
812
+ threadsLoadingState.setLoading(false);
813
+ endLoading();
661
814
  }
662
815
  }
663
816
  function isValidUUID(uuid) {
@@ -672,24 +825,25 @@ export function createDiscussionStoreDefinition(config) {
672
825
  async function loadThread(spaceSlug, threadId) {
673
826
  latestThreadId.value = threadId;
674
827
  cleanupRealtimeChannels();
675
- loading.value = true;
828
+ beginLoading();
676
829
  error.value = null;
830
+ replies.value = [];
831
+ repliesNextCursor.value = null;
832
+ currentReplySort.value = 'best';
677
833
  if (!threadId) {
678
834
  error.value = 'Missing thread identifier';
679
- loading.value = false;
835
+ endLoading();
680
836
  return null;
681
837
  }
682
838
  // Accept both UUID and slug identifiers — the backend resolves by ID or slug
683
839
  if (!/^[\w-]+$/u.test(threadId)) {
684
840
  error.value = 'Invalid thread identifier format';
685
- loading.value = false;
841
+ endLoading();
686
842
  return null;
687
843
  }
688
- let isStale = false;
689
844
  try {
690
845
  const response = await client.get(`/v1/discussion/threads/${threadId}`);
691
846
  if (latestThreadId.value !== threadId) {
692
- isStale = true;
693
847
  return null;
694
848
  }
695
849
  const responseData = toRecord(response.data);
@@ -739,7 +893,6 @@ export function createDiscussionStoreDefinition(config) {
739
893
  }
740
894
  catch (err) {
741
895
  if (latestThreadId.value !== threadId) {
742
- isStale = true;
743
896
  return null;
744
897
  }
745
898
  const errorResponseData = getErrorResponseData(err);
@@ -765,18 +918,15 @@ export function createDiscussionStoreDefinition(config) {
765
918
  throw err;
766
919
  }
767
920
  finally {
768
- if (!isStale) {
769
- loading.value = false;
770
- }
921
+ endLoading();
771
922
  }
772
923
  }
773
924
  async function loadReplies(threadId, cursor, sortBy = 'best') {
774
925
  if (latestThreadId.value === null) {
775
926
  latestThreadId.value = threadId;
776
927
  }
777
- loading.value = true;
928
+ beginLoading();
778
929
  error.value = null;
779
- let isStale = false;
780
930
  try {
781
931
  const queryParts = [];
782
932
  if (cursor) {
@@ -788,7 +938,6 @@ export function createDiscussionStoreDefinition(config) {
788
938
  const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
789
939
  const response = await client.get(`/v1/discussion/threads/${threadId}/replies${queryString}`);
790
940
  if (latestThreadId.value !== threadId) {
791
- isStale = true;
792
941
  return undefined;
793
942
  }
794
943
  const responseData = toRecord(response.data);
@@ -848,7 +997,6 @@ export function createDiscussionStoreDefinition(config) {
848
997
  }
849
998
  catch (err) {
850
999
  if (latestThreadId.value !== threadId) {
851
- isStale = true;
852
1000
  return undefined;
853
1001
  }
854
1002
  logger.error('Failed to load replies:', err);
@@ -856,9 +1004,7 @@ export function createDiscussionStoreDefinition(config) {
856
1004
  throw err;
857
1005
  }
858
1006
  finally {
859
- if (!isStale) {
860
- loading.value = false;
861
- }
1007
+ endLoading();
862
1008
  }
863
1009
  }
864
1010
  function insertTopLevelReply(reply) {
@@ -873,10 +1019,17 @@ export function createDiscussionStoreDefinition(config) {
873
1019
  if (existingReplyIndex !== -1) {
874
1020
  const existingReply = replies.value[existingReplyIndex];
875
1021
  if (existingReply) {
876
- replies.value[existingReplyIndex] = {
1022
+ const merged = {
877
1023
  ...existingReply,
878
1024
  ...reply,
879
1025
  };
1026
+ // Broadcast payloads lack an authenticated user context, so
1027
+ // is_author is always false in them. Preserve the value from
1028
+ // the original authenticated API response when it was true.
1029
+ if (hasReplyAuthorFlag(existingReply) && !hasReplyAuthorFlag(reply)) {
1030
+ setReplyAuthorFlag(merged, true);
1031
+ }
1032
+ replies.value[existingReplyIndex] = merged;
880
1033
  }
881
1034
  return false;
882
1035
  }
@@ -914,7 +1067,7 @@ export function createDiscussionStoreDefinition(config) {
914
1067
  }
915
1068
  async function createThread(spaceSlug, input) {
916
1069
  cleanupRealtimeChannels();
917
- loading.value = true;
1070
+ beginLoading();
918
1071
  error.value = null;
919
1072
  try {
920
1073
  const response = await client.post(`/v1/discussion/spaces/${spaceSlug}/threads`, {
@@ -942,11 +1095,11 @@ export function createDiscussionStoreDefinition(config) {
942
1095
  throw err;
943
1096
  }
944
1097
  finally {
945
- loading.value = false;
1098
+ endLoading();
946
1099
  }
947
1100
  }
948
1101
  async function loadTagCategories(query = '') {
949
- loading.value = true;
1102
+ beginLoading();
950
1103
  error.value = null;
951
1104
  try {
952
1105
  const normalizedQuery = query.trim();
@@ -980,7 +1133,7 @@ export function createDiscussionStoreDefinition(config) {
980
1133
  throw err;
981
1134
  }
982
1135
  finally {
983
- loading.value = false;
1136
+ endLoading();
984
1137
  }
985
1138
  }
986
1139
  function cleanupRealtimeChannels() {
@@ -1222,10 +1375,17 @@ export function createDiscussionStoreDefinition(config) {
1222
1375
  if (!existingReply) {
1223
1376
  return;
1224
1377
  }
1225
- replies.value[replyIndex] = normalizeReplyPayload({
1378
+ const merged = {
1226
1379
  ...existingReply,
1227
1380
  ...payload,
1228
- }, existingReply);
1381
+ };
1382
+ // Broadcast payloads lack an authenticated user context, so
1383
+ // is_author is always false in them. Preserve the value from
1384
+ // the original authenticated API response when it was true.
1385
+ if (hasReplyAuthorFlag(existingReply) && !hasReplyAuthorFlag(payload)) {
1386
+ setReplyAuthorFlag(merged, true);
1387
+ }
1388
+ replies.value[replyIndex] = normalizeReplyPayload(merged, existingReply);
1229
1389
  }
1230
1390
  function handleRealtimeReplyReaction(payload) {
1231
1391
  if (!payload?.reply_id || !payload.thread_id || !payload.user_id) {
@@ -1344,40 +1504,49 @@ export function createDiscussionStoreDefinition(config) {
1344
1504
  throw err;
1345
1505
  }
1346
1506
  }
1347
- async function searchThreads(query, spaceSlug) {
1348
- loading.value = true;
1507
+ async function searchThreads(query, spaceSlug, options) {
1508
+ beginLoading();
1349
1509
  error.value = null;
1350
1510
  try {
1351
1511
  const params = new URLSearchParams({ q: query });
1352
1512
  if (spaceSlug) {
1353
1513
  params.append('space', spaceSlug);
1354
1514
  }
1355
- const response = await client.get(`/v1/discussion/search/threads?${params.toString()}`);
1515
+ const requestConfig = options?.signal
1516
+ ? { signal: options.signal }
1517
+ : undefined;
1518
+ const response = await client.get(`/v1/discussion/search/threads?${params.toString()}`, requestConfig);
1356
1519
  return response.data;
1357
1520
  }
1358
1521
  catch (err) {
1522
+ if (options?.signal?.aborted) {
1523
+ return undefined;
1524
+ }
1359
1525
  logger.error('Failed to search threads:', err);
1360
1526
  error.value = getErrorMessage(err, 'Failed to search');
1361
1527
  throw err;
1362
1528
  }
1363
1529
  finally {
1364
- loading.value = false;
1530
+ endLoading();
1365
1531
  }
1366
1532
  }
1367
- async function searchThreadsInSpace(query, spaceId) {
1533
+ async function searchThreadsInSpace(query, spaceId, options) {
1368
1534
  const normalizedQuery = query.trim();
1369
1535
  if (normalizedQuery.length < 2) {
1370
1536
  return [];
1371
1537
  }
1372
- loading.value = true;
1538
+ beginLoading();
1373
1539
  error.value = null;
1374
1540
  try {
1541
+ const requestConfig = options?.signal
1542
+ ? { signal: options.signal }
1543
+ : undefined;
1375
1544
  const response = await client.post('/v1/discussion/search', {
1376
1545
  query: normalizedQuery,
1377
1546
  space_id: spaceId,
1378
1547
  sort_by: 'relevance',
1379
1548
  limit: 50,
1380
- });
1549
+ }, requestConfig);
1381
1550
  const rows = extractThreadSearchRows(response.data);
1382
1551
  if (rows.length > 0) {
1383
1552
  return rows.map((row) => mapSearchRowToThread(row, spaceId));
@@ -1385,6 +1554,9 @@ export function createDiscussionStoreDefinition(config) {
1385
1554
  return filterLoadedThreadsByQuery(normalizedQuery, spaceId);
1386
1555
  }
1387
1556
  catch (err) {
1557
+ if (options?.signal?.aborted) {
1558
+ return [];
1559
+ }
1388
1560
  logger.error('Failed to search threads in space:', err);
1389
1561
  error.value = isAxiosError(err)
1390
1562
  ? err.response?.data?.message ?? 'Failed to search threads'
@@ -1392,25 +1564,31 @@ export function createDiscussionStoreDefinition(config) {
1392
1564
  throw err;
1393
1565
  }
1394
1566
  finally {
1395
- loading.value = false;
1567
+ endLoading();
1396
1568
  }
1397
1569
  }
1398
- async function searchThreadsGlobally(query) {
1570
+ async function searchThreadsGlobally(query, options) {
1399
1571
  const normalizedQuery = query.trim();
1400
1572
  if (normalizedQuery.length < 2) {
1401
1573
  return [];
1402
1574
  }
1403
- loading.value = true;
1575
+ beginLoading();
1404
1576
  error.value = null;
1405
1577
  try {
1578
+ const requestConfig = options?.signal
1579
+ ? { signal: options.signal }
1580
+ : undefined;
1406
1581
  const response = await client.post('/v1/discussion/search', {
1407
1582
  query: normalizedQuery,
1408
1583
  sort_by: 'relevance',
1409
1584
  limit: 50,
1410
- });
1585
+ }, requestConfig);
1411
1586
  return extractThreadSearchRows(response.data).map((row) => mapSearchRowToThread(row));
1412
1587
  }
1413
1588
  catch (err) {
1589
+ if (options?.signal?.aborted) {
1590
+ return [];
1591
+ }
1414
1592
  logger.error('Failed to search threads globally:', err);
1415
1593
  error.value = isAxiosError(err)
1416
1594
  ? err.response?.data?.message ?? 'Failed to search threads'
@@ -1418,7 +1596,7 @@ export function createDiscussionStoreDefinition(config) {
1418
1596
  throw err;
1419
1597
  }
1420
1598
  finally {
1421
- loading.value = false;
1599
+ endLoading();
1422
1600
  }
1423
1601
  }
1424
1602
  function mapSearchRowToThread(row, fallbackSpaceId) {
@@ -1446,6 +1624,14 @@ export function createDiscussionStoreDefinition(config) {
1446
1624
  if (typeof row.slug === 'string') {
1447
1625
  thread.slug = row.slug;
1448
1626
  }
1627
+ const spaceId = row.space_id ?? fallbackSpaceId ?? '';
1628
+ if (typeof row.space_name === 'string' && typeof row.space_slug === 'string' && spaceId) {
1629
+ thread.space = {
1630
+ id: spaceId,
1631
+ slug: row.space_slug,
1632
+ name: row.space_name,
1633
+ };
1634
+ }
1449
1635
  return thread;
1450
1636
  }
1451
1637
  function extractThreadSearchRows(responseBody) {
@@ -1481,7 +1667,7 @@ export function createDiscussionStoreDefinition(config) {
1481
1667
  return spaces.value.filter((space) => space.meta?.is_featured);
1482
1668
  }
1483
1669
  async function setThreadPinned(threadId, pinned) {
1484
- loading.value = true;
1670
+ beginLoading();
1485
1671
  error.value = null;
1486
1672
  try {
1487
1673
  await client.post(`/v1/discussion/threads/${threadId}/pin`, { pinned });
@@ -1500,11 +1686,11 @@ export function createDiscussionStoreDefinition(config) {
1500
1686
  throw err;
1501
1687
  }
1502
1688
  finally {
1503
- loading.value = false;
1689
+ endLoading();
1504
1690
  }
1505
1691
  }
1506
1692
  async function setThreadLocked(threadId, locked) {
1507
- loading.value = true;
1693
+ beginLoading();
1508
1694
  error.value = null;
1509
1695
  try {
1510
1696
  await client.post(`/v1/discussion/threads/${threadId}/lock`, { locked });
@@ -1523,11 +1709,11 @@ export function createDiscussionStoreDefinition(config) {
1523
1709
  throw err;
1524
1710
  }
1525
1711
  finally {
1526
- loading.value = false;
1712
+ endLoading();
1527
1713
  }
1528
1714
  }
1529
1715
  async function moveThread(threadId, toSpaceSlug) {
1530
- loading.value = true;
1716
+ beginLoading();
1531
1717
  error.value = null;
1532
1718
  try {
1533
1719
  const response = await client.post(`/v1/discussion/threads/${threadId}/move`, {
@@ -1536,11 +1722,21 @@ export function createDiscussionStoreDefinition(config) {
1536
1722
  const responseData = toRecord(response.data);
1537
1723
  const movedThread = (responseData?.data ?? null);
1538
1724
  const destinationSpace = spaces.value.find((space) => space.slug === toSpaceSlug);
1725
+ const destinationSpaceSummary = destinationSpace
1726
+ ? {
1727
+ id: destinationSpace.id,
1728
+ slug: destinationSpace.slug,
1729
+ name: destinationSpace.name,
1730
+ }
1731
+ : null;
1539
1732
  if (currentThread.value?.id === threadId && currentThread.value) {
1540
1733
  currentThread.value = {
1541
1734
  ...currentThread.value,
1542
1735
  ...(movedThread ?? {}),
1543
- ...(destinationSpace ? { space_id: destinationSpace.id } : {}),
1736
+ ...(destinationSpaceSummary ? {
1737
+ space_id: destinationSpaceSummary.id,
1738
+ space: destinationSpaceSummary,
1739
+ } : {}),
1544
1740
  };
1545
1741
  }
1546
1742
  const currentSpaceSlug = currentSpace.value?.slug ?? null;
@@ -1557,7 +1753,10 @@ export function createDiscussionStoreDefinition(config) {
1557
1753
  threads.value[threadIndex] = {
1558
1754
  ...existingThread,
1559
1755
  ...(movedThread ?? {}),
1560
- ...(destinationSpace ? { space_id: destinationSpace.id } : {}),
1756
+ ...(destinationSpaceSummary ? {
1757
+ space_id: destinationSpaceSummary.id,
1758
+ space: destinationSpaceSummary,
1759
+ } : {}),
1561
1760
  };
1562
1761
  }
1563
1762
  }
@@ -1569,11 +1768,11 @@ export function createDiscussionStoreDefinition(config) {
1569
1768
  throw err;
1570
1769
  }
1571
1770
  finally {
1572
- loading.value = false;
1771
+ endLoading();
1573
1772
  }
1574
1773
  }
1575
1774
  async function updateThread(threadId, updates) {
1576
- loading.value = true;
1775
+ beginLoading();
1577
1776
  error.value = null;
1578
1777
  try {
1579
1778
  const response = await client.patch(`/v1/discussion/threads/${threadId}`, {
@@ -1603,11 +1802,11 @@ export function createDiscussionStoreDefinition(config) {
1603
1802
  throw err;
1604
1803
  }
1605
1804
  finally {
1606
- loading.value = false;
1805
+ endLoading();
1607
1806
  }
1608
1807
  }
1609
1808
  async function deleteThread(threadId) {
1610
- loading.value = true;
1809
+ beginLoading();
1611
1810
  error.value = null;
1612
1811
  try {
1613
1812
  const response = await client.delete(`/v1/discussion/threads/${threadId}`);
@@ -1623,11 +1822,11 @@ export function createDiscussionStoreDefinition(config) {
1623
1822
  throw err;
1624
1823
  }
1625
1824
  finally {
1626
- loading.value = false;
1825
+ endLoading();
1627
1826
  }
1628
1827
  }
1629
1828
  async function restoreThread(threadId) {
1630
- loading.value = true;
1829
+ beginLoading();
1631
1830
  error.value = null;
1632
1831
  try {
1633
1832
  const response = await client.post(`/v1/discussion/threads/${threadId}/restore`);
@@ -1668,11 +1867,11 @@ export function createDiscussionStoreDefinition(config) {
1668
1867
  throw err;
1669
1868
  }
1670
1869
  finally {
1671
- loading.value = false;
1870
+ endLoading();
1672
1871
  }
1673
1872
  }
1674
1873
  async function deleteReply(replyId) {
1675
- loading.value = true;
1874
+ beginLoading();
1676
1875
  error.value = null;
1677
1876
  const replyIndex = replies.value.findIndex((reply) => reply.id === replyId);
1678
1877
  const replyToRestore = replyIndex >= 0 ? replies.value[replyIndex] ?? null : null;
@@ -1730,7 +1929,7 @@ export function createDiscussionStoreDefinition(config) {
1730
1929
  throw err;
1731
1930
  }
1732
1931
  finally {
1733
- loading.value = false;
1932
+ endLoading();
1734
1933
  }
1735
1934
  }
1736
1935
  async function updateReply(replyId, body) {
@@ -1963,16 +2162,23 @@ export function createDiscussionStoreDefinition(config) {
1963
2162
  spaceMembershipLoading,
1964
2163
  cleanupRealtimeChannels,
1965
2164
  threads,
2165
+ browseThreadList,
2166
+ currentBrowseMode,
1966
2167
  currentThread,
1967
2168
  replies,
2169
+ currentReplySort,
1968
2170
  loading,
1969
2171
  error,
1970
2172
  nextCursor,
2173
+ browseNextCursor,
1971
2174
  repliesNextCursor,
1972
2175
  spacesLoadingState,
1973
2176
  threadsLoadingState,
1974
2177
  repliesLoadingState,
1975
2178
  loadSpaces,
2179
+ createSpace,
2180
+ moveSpace,
2181
+ reorderSpaces,
1976
2182
  loadSpaceDetail,
1977
2183
  loadSpaceMembership,
1978
2184
  joinSpace,
@@ -1982,6 +2188,7 @@ export function createDiscussionStoreDefinition(config) {
1982
2188
  rootSpaces,
1983
2189
  leafSpaces,
1984
2190
  loadThreads,
2191
+ browseThreads,
1985
2192
  loadThread,
1986
2193
  loadReplies,
1987
2194
  createThread,