cozy-sharing 37.5.4 → 37.6.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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Pick the most relevant source for a recipient: prefer the target's own
3
+ * share ("self"), fall back to the first ancestor source.
4
+ * Needed because revoke/RO-RW actions target a specific sharing+member index.
5
+ */
6
+ const getBestSource = effectiveRecipient => {
7
+ const sources = effectiveRecipient.sources || []
8
+ return sources.find(s => s.kind === 'self') || sources[0]
9
+ }
10
+
11
+ /**
12
+ * Build a view object for MemberRecipient/MemberRecipientPermissions.
13
+ * Extracts sharingId/memberIndex/avatarPath/type from the source so
14
+ * existing components work without reading sources[] directly.
15
+ * Does not spread the backend data — only the fields needed for rendering.
16
+ */
17
+ const mapToRecipient = (effectiveRecipient, source) => ({
18
+ name: effectiveRecipient.name,
19
+ email: effectiveRecipient.email,
20
+ instance: effectiveRecipient.instance,
21
+ status: effectiveRecipient.status,
22
+ read_only: effectiveRecipient.read_only,
23
+ index: `effective-${source.sharing_id}-${source.member_index}`,
24
+ sharingId: source.sharing_id,
25
+ memberIndex: source.member_index,
26
+ avatarPath: `/sharings/${source.sharing_id}/recipients/${source.member_index}/avatar`,
27
+ type: effectiveRecipient.read_only ? 'one-way' : 'two-way'
28
+ })
29
+
30
+ export const mapEffectiveRecipients = effectiveRecipients =>
31
+ (effectiveRecipients || []).map(recipient => {
32
+ const source = getBestSource(recipient)
33
+ return source ? mapToRecipient(recipient, source) : recipient
34
+ })
@@ -0,0 +1,125 @@
1
+ import { mapEffectiveRecipients } from './effectiveRecipients'
2
+
3
+ const makeAncestorSource = (rootName = 'Parent Folder') => ({
4
+ sharing_id: 'sharing-1',
5
+ root_id: 'root-1',
6
+ root_name: rootName,
7
+ kind: 'ancestor',
8
+ member_index: 1,
9
+ read_only: false,
10
+ manageable: false
11
+ })
12
+
13
+ const makeSelfSource = (opts = {}) => ({
14
+ sharing_id: opts.sharing_id || 'sharing-2',
15
+ root_id: 'root-2',
16
+ root_name: 'Child Folder',
17
+ kind: 'self',
18
+ member_index: opts.memberIndex ?? 1,
19
+ read_only: opts.readOnly ?? false,
20
+ manageable: opts.manageable ?? true
21
+ })
22
+
23
+ const makeRecipient = (overrides = {}) => ({
24
+ id: 'recip-1',
25
+ name: 'Alice',
26
+ email: 'alice@example.com',
27
+ instance: 'https://alice.mycozy.cloud',
28
+ status: 'ready',
29
+ read_only: false,
30
+ can_edit_here: false,
31
+ sources: overrides.sources || [],
32
+ ...overrides
33
+ })
34
+
35
+ describe('mapEffectiveRecipients', () => {
36
+ it('returns empty array for empty input', () => {
37
+ expect(mapEffectiveRecipients([])).toEqual([])
38
+ })
39
+
40
+ it('returns empty array for null or undefined', () => {
41
+ expect(mapEffectiveRecipients(null)).toEqual([])
42
+ expect(mapEffectiveRecipients(undefined)).toEqual([])
43
+ })
44
+
45
+ it('maps a recipient with a self source using the self source', () => {
46
+ const recipient = makeRecipient({
47
+ sources: [makeSelfSource()]
48
+ })
49
+ const result = mapEffectiveRecipients([recipient])
50
+
51
+ expect(result).toHaveLength(1)
52
+ expect(result[0].sharingId).toBe('sharing-2')
53
+ expect(result[0].memberIndex).toBe(1)
54
+ expect(result[0].avatarPath).toBe('/sharings/sharing-2/recipients/1/avatar')
55
+ expect(result[0].type).toBe('two-way')
56
+ })
57
+
58
+ it('maps a recipient with only ancestor sources using the ancestor source', () => {
59
+ const recipient = makeRecipient({
60
+ sources: [makeAncestorSource('Shared Parent')]
61
+ })
62
+ const result = mapEffectiveRecipients([recipient])
63
+
64
+ expect(result).toHaveLength(1)
65
+ expect(result[0].sharingId).toBe('sharing-1')
66
+ expect(result[0].memberIndex).toBe(1)
67
+ expect(result[0].name).toBe('Alice')
68
+ expect(result[0].email).toBe('alice@example.com')
69
+ })
70
+
71
+ it('maps an owner (self source, index 0) correctly', () => {
72
+ const recipient = makeRecipient({
73
+ status: 'owner',
74
+ read_only: true,
75
+ sources: [makeSelfSource({ memberIndex: 0, manageable: false })]
76
+ })
77
+ const result = mapEffectiveRecipients([recipient])
78
+
79
+ expect(result).toHaveLength(1)
80
+ expect(result[0].status).toBe('owner')
81
+ expect(result[0].type).toBe('one-way')
82
+ })
83
+
84
+ it('maps an owner with only ancestor sources correctly', () => {
85
+ const recipient = makeRecipient({
86
+ status: 'owner',
87
+ sources: [makeAncestorSource('Parent Folder')]
88
+ })
89
+ const result = mapEffectiveRecipients([recipient])
90
+
91
+ expect(result).toHaveLength(1)
92
+ expect(result[0].status).toBe('owner')
93
+ expect(result[0].sharingId).toBe('sharing-1')
94
+ expect(result[0].memberIndex).toBe(1)
95
+ })
96
+
97
+ it('sets type to one-way for read_only recipients', () => {
98
+ const recipient = makeRecipient({
99
+ read_only: true,
100
+ sources: [makeSelfSource({ readOnly: true })]
101
+ })
102
+ const result = mapEffectiveRecipients([recipient])
103
+ expect(result[0].type).toBe('one-way')
104
+ })
105
+
106
+ it('handles all recipients in the same array regardless of source kind', () => {
107
+ const selfRecipient = makeRecipient({
108
+ id: 'self-1',
109
+ name: 'Bob',
110
+ email: 'bob@example.com',
111
+ sources: [makeSelfSource()]
112
+ })
113
+ const ancestorRecipient = makeRecipient({
114
+ id: 'ancestor-1',
115
+ name: 'Carol',
116
+ email: 'carol@example.com',
117
+ sources: [makeAncestorSource('Root Folder')]
118
+ })
119
+ const result = mapEffectiveRecipients([selfRecipient, ancestorRecipient])
120
+
121
+ expect(result).toHaveLength(2)
122
+ expect(result[0].sharingId).toBe('sharing-2')
123
+ expect(result[1].sharingId).toBe('sharing-1')
124
+ })
125
+ })
package/src/state.js CHANGED
@@ -13,6 +13,9 @@ const ADD_SHARING_LINK = 'ADD_SHARING_LINK'
13
13
  const UPDATE_SHARING_LINK = 'UPDATE_SHARING_LINK'
14
14
  const REVOKE_SHARING_LINK = 'REVOKE_SHARING_LINK'
15
15
  const RECEIVE_PATHS = 'RECEIVE_PATHS'
16
+ const RECEIVE_EFFECTIVE_RECIPIENTS = 'RECEIVE_EFFECTIVE_RECIPIENTS'
17
+ const UPDATE_EFFECTIVE_RECIPIENT = 'UPDATE_EFFECTIVE_RECIPIENT'
18
+ const INVALIDATE_EFFECTIVE_RECIPIENTS = 'INVALIDATE_EFFECTIVE_RECIPIENTS'
16
19
  export const SHARING_TYPE = {
17
20
  TWO_WAY: 'two-way',
18
21
  ONE_WAY: 'one-way'
@@ -101,6 +104,21 @@ export const revokeSharingLink = permissions => ({
101
104
  permissions
102
105
  })
103
106
  export const receivePaths = paths => ({ type: RECEIVE_PATHS, paths })
107
+ export const receiveEffectiveRecipients = (docId, recipients) => ({
108
+ type: RECEIVE_EFFECTIVE_RECIPIENTS,
109
+ docId,
110
+ recipients
111
+ })
112
+ export const updateEffectiveRecipient = (sharingId, memberIndex, readOnly) => ({
113
+ type: UPDATE_EFFECTIVE_RECIPIENT,
114
+ sharingId,
115
+ memberIndex,
116
+ readOnly
117
+ })
118
+ export const invalidateEffectiveRecipients = docId => ({
119
+ type: INVALIDATE_EFFECTIVE_RECIPIENTS,
120
+ docId
121
+ })
104
122
  export const matchingInstanceName =
105
123
  (instanceUri = '') =>
106
124
  shareMember =>
@@ -281,11 +299,41 @@ const sharedPaths = (state = [], action) => {
281
299
  }
282
300
  }
283
301
 
302
+ const effectiveRecipients = (state = {}, action) => {
303
+ switch (action.type) {
304
+ case RECEIVE_EFFECTIVE_RECIPIENTS:
305
+ return { ...state, [action.docId]: action.recipients }
306
+ case UPDATE_EFFECTIVE_RECIPIENT:
307
+ return Object.fromEntries(
308
+ Object.entries(state).map(([docId, recipients]) => [
309
+ docId,
310
+ recipients.map(r =>
311
+ r.sharingId === action.sharingId &&
312
+ r.memberIndex === action.memberIndex
313
+ ? {
314
+ ...r,
315
+ read_only: action.readOnly,
316
+ type: action.readOnly ? 'one-way' : 'two-way'
317
+ }
318
+ : r
319
+ )
320
+ ])
321
+ )
322
+ case INVALIDATE_EFFECTIVE_RECIPIENTS: {
323
+ const { [action.docId]: _, ...rest } = state
324
+ return rest
325
+ }
326
+ default:
327
+ return state
328
+ }
329
+ }
330
+
284
331
  const reducer = (state = {}, action = {}) => ({
285
332
  byDocId: byDocId(state.byDocId, action),
286
333
  sharings: sharings(state.sharings, action),
287
334
  permissions: permissions(state.permissions, action),
288
- sharedPaths: sharedPaths(state.sharedPaths, action)
335
+ sharedPaths: sharedPaths(state.sharedPaths, action),
336
+ effectiveRecipients: effectiveRecipients(state.effectiveRecipients, action)
289
337
  })
290
338
  export default reducer
291
339
 
@@ -527,6 +575,9 @@ export const getSharedParentPath = (state, documentPath) => {
527
575
  return null
528
576
  }
529
577
 
578
+ export const getEffectiveRecipients = (state, docId) =>
579
+ state.effectiveRecipients?.[docId] || []
580
+
530
581
  // helpers
531
582
  export const getSharedDocIds = doc =>
532
583
  doc.type === 'io.cozy.sharings'
package/src/state.spec.js CHANGED
@@ -42,7 +42,8 @@ describe('Sharing state', () => {
42
42
  byDocId: {},
43
43
  sharings: [],
44
44
  permissions: [],
45
- sharedPaths: []
45
+ sharedPaths: [],
46
+ effectiveRecipients: {}
46
47
  })
47
48
  })
48
49