@oceanum/oceanumlab 4.7.1 → 4.8.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.
Files changed (51) hide show
  1. package/lib/DatameshWidget.d.ts +11 -0
  2. package/lib/DatameshWidget.js +35 -5
  3. package/lib/StoredNotebooks.d.ts +27 -5
  4. package/lib/StoredNotebooks.js +141 -24
  5. package/lib/auth/serverAuth.js +1 -0
  6. package/lib/auth/startSignIn.d.ts +11 -0
  7. package/lib/auth/startSignIn.js +16 -0
  8. package/lib/auth/tokens.d.ts +13 -0
  9. package/lib/index.d.ts +1 -1
  10. package/lib/index.js +43 -7
  11. package/lib/share/client.d.ts +86 -0
  12. package/lib/share/client.js +206 -0
  13. package/lib/share/events.d.ts +7 -0
  14. package/lib/share/events.js +9 -0
  15. package/lib/share/notebook.d.ts +216 -0
  16. package/lib/share/notebook.js +355 -0
  17. package/lib/share/oceanumOnly.d.ts +72 -0
  18. package/lib/share/oceanumOnly.js +127 -0
  19. package/lib/share/plugin.d.ts +23 -0
  20. package/lib/share/plugin.js +981 -0
  21. package/lib/share/upload.d.ts +6 -0
  22. package/lib/share/upload.js +27 -0
  23. package/lib/share/widgets.d.ts +25 -0
  24. package/lib/share/widgets.js +154 -0
  25. package/package.json +5 -1
  26. package/schema/datamesh-connect.json +6 -0
  27. package/src/DatameshWidget.tsx +79 -2
  28. package/src/StoredNotebooks.tsx +274 -24
  29. package/src/__tests__/OceanumPanel.spec.tsx +159 -5
  30. package/src/__tests__/StoredNotebooks.spec.tsx +528 -0
  31. package/src/__tests__/oceanumOnly.spec.ts +317 -0
  32. package/src/__tests__/settingsSchema.spec.ts +11 -1
  33. package/src/__tests__/shareClient.spec.ts +328 -0
  34. package/src/__tests__/shareNotebook.spec.ts +624 -0
  35. package/src/__tests__/sharePlugin.spec.ts +664 -0
  36. package/src/auth/serverAuth.ts +1 -0
  37. package/src/auth/startSignIn.ts +23 -0
  38. package/src/auth/tokens.ts +13 -0
  39. package/src/index.ts +43 -7
  40. package/src/share/client.ts +291 -0
  41. package/src/share/events.ts +10 -0
  42. package/src/share/notebook.ts +525 -0
  43. package/src/share/oceanumOnly.ts +177 -0
  44. package/src/share/plugin.ts +1220 -0
  45. package/src/share/upload.ts +27 -0
  46. package/src/share/widgets.ts +217 -0
  47. package/style/index.css +212 -18
  48. package/lib/specStore.d.ts +0 -37
  49. package/lib/specStore.js +0 -57
  50. package/src/__tests__/specStore.spec.ts +0 -126
  51. package/src/specStore.ts +0 -82
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Ask the user for a notebook file on their computer; null if they cancel.
3
+ *
4
+ * Kept apart from the plugin because it is browser-only: the tests replace it.
5
+ */
6
+ export function chooseNotebookFile(): Promise<File | null> {
7
+ return new Promise(resolve => {
8
+ const input = document.createElement('input');
9
+ input.type = 'file';
10
+ input.accept = '.ipynb,application/x-ipynb+json';
11
+ input.hidden = true;
12
+ // Attached while the picker is open: some browsers (Safari) drop the `change`
13
+ // event of a detached input, or collect the input before it fires.
14
+ document.body.appendChild(input);
15
+ const settle = (file: File | null): void => {
16
+ input.remove();
17
+ resolve(file);
18
+ };
19
+ input.addEventListener('change', () => settle(input.files?.[0] ?? null), {
20
+ once: true
21
+ });
22
+ // Browsers fire `cancel` when the picker is dismissed; without it the promise
23
+ // would simply never settle, which is harmless.
24
+ input.addEventListener('cancel', () => settle(null), { once: true });
25
+ input.click();
26
+ });
27
+ }
@@ -0,0 +1,217 @@
1
+ import { Clipboard, Dialog } from '@jupyterlab/apputils';
2
+ import { Widget } from '@lumino/widgets';
3
+
4
+ import type { PermissionGrant, ShareChoice } from './client';
5
+ import { ISpecSummary, parseShareEmails, parseTimestamp } from './notebook';
6
+
7
+ function element<K extends keyof HTMLElementTagNameMap>(
8
+ tag: K,
9
+ className: string,
10
+ text?: string
11
+ ): HTMLElementTagNameMap[K] {
12
+ const node = document.createElement(tag);
13
+ node.className = className;
14
+ if (text !== undefined) {
15
+ // Names and emails come from other users: always set text, never HTML.
16
+ node.textContent = text;
17
+ }
18
+ return node;
19
+ }
20
+
21
+ /** Dialog body listing notebooks to open; its value is the selected record id. */
22
+ export class NotebookListBody
23
+ extends Widget
24
+ implements Dialog.IBodyWidget<string | null>
25
+ {
26
+ constructor(mine: readonly ISpecSummary[], shared: readonly ISpecSummary[]) {
27
+ super();
28
+ this.addClass('jp-OceanumShare-body');
29
+ this._addSection('My notebooks', mine, false);
30
+ this._addSection('Shared with me', shared, true);
31
+ }
32
+
33
+ getValue(): string | null {
34
+ return this._selected;
35
+ }
36
+
37
+ private _addSection(
38
+ title: string,
39
+ items: readonly ISpecSummary[],
40
+ showCreator: boolean
41
+ ): void {
42
+ this.node.appendChild(element('h3', 'jp-OceanumShare-heading', title));
43
+ if (items.length === 0) {
44
+ this.node.appendChild(element('p', 'jp-OceanumShare-empty', 'None'));
45
+ return;
46
+ }
47
+ const list = element('ul', 'jp-OceanumShare-list');
48
+ list.setAttribute('role', 'listbox');
49
+ list.setAttribute('aria-label', title);
50
+ for (const item of items) {
51
+ const option = element('li', 'jp-OceanumShare-item');
52
+ option.setAttribute('role', 'option');
53
+ option.setAttribute('aria-selected', 'false');
54
+ option.tabIndex = 0;
55
+ option.appendChild(element('span', 'jp-OceanumShare-name', item.name));
56
+ const modified = parseTimestamp(item.modified);
57
+ const details = [
58
+ modified ? `Modified ${modified.toLocaleDateString()}` : '',
59
+ showCreator && item.creator ? item.creator : ''
60
+ ].filter(Boolean);
61
+ option.appendChild(
62
+ element('span', 'jp-OceanumShare-meta', details.join(' · '))
63
+ );
64
+ const select = () => this._select(item.id, option);
65
+ option.addEventListener('click', select);
66
+ option.addEventListener('focus', select);
67
+ list.appendChild(option);
68
+ }
69
+ this.node.appendChild(list);
70
+ }
71
+
72
+ private _select(id: string, option: HTMLElement): void {
73
+ this._selected = id;
74
+ for (const node of this.node.querySelectorAll('.jp-OceanumShare-item')) {
75
+ const selected = node === option;
76
+ node.classList.toggle('jp-mod-selected', selected);
77
+ node.setAttribute('aria-selected', String(selected));
78
+ }
79
+ }
80
+
81
+ private _selected: string | null = null;
82
+ }
83
+
84
+ export const EVERYONE_WARNING =
85
+ 'Anyone with the link can read this notebook, including people without an Oceanum ' +
86
+ 'account. This cannot be limited to your organisation. Its name and your email ' +
87
+ 'address also become visible to anyone who lists Oceanum notebooks.';
88
+
89
+ const PUBLIC_GRANT: PermissionGrant = {
90
+ type: 'public',
91
+ entity: '',
92
+ permission: 'read'
93
+ };
94
+
95
+ /** Dialog body for sharing; its value is the change to make, or `null` if incomplete. */
96
+ export class ShareBody
97
+ extends Widget
98
+ implements Dialog.IBodyWidget<ShareChoice | null>
99
+ {
100
+ constructor(link: string) {
101
+ super();
102
+ this.addClass('jp-OceanumShare-body');
103
+
104
+ this.node.appendChild(element('h3', 'jp-OceanumShare-heading', 'Link'));
105
+ const row = element('div', 'jp-OceanumShare-linkRow');
106
+ const linkInput = element('input', 'jp-OceanumShare-link');
107
+ linkInput.readOnly = true;
108
+ linkInput.value = link;
109
+ linkInput.setAttribute('aria-label', 'Notebook link');
110
+ const copy = element(
111
+ 'button',
112
+ 'jp-mod-styled jp-OceanumShare-copy',
113
+ 'Copy link'
114
+ );
115
+ copy.type = 'button';
116
+ copy.addEventListener('click', () => {
117
+ Clipboard.copyToSystem(link);
118
+ copy.textContent = 'Copied';
119
+ });
120
+ row.append(linkInput, copy);
121
+ this.node.appendChild(row);
122
+ this.node.appendChild(
123
+ element(
124
+ 'p',
125
+ 'jp-OceanumShare-hint',
126
+ 'The link opens the notebook for anyone it is shared with.'
127
+ )
128
+ );
129
+
130
+ this.node.appendChild(
131
+ element('h3', 'jp-OceanumShare-heading', 'Share with')
132
+ );
133
+ const name = `jp-OceanumShare-who-${++ShareBody._count}`;
134
+ this._person = this._radio(name, 'A specific person', true);
135
+ const personRow = element('div', 'jp-OceanumShare-personRow');
136
+ this._email = element('textarea', 'jp-OceanumShare-email');
137
+ this._email.rows = 2;
138
+ this._email.placeholder = 'name@example.com, another@example.com';
139
+ this._email.setAttribute('aria-label', 'Email addresses');
140
+ this._level = element('select', 'jp-OceanumShare-level');
141
+ this._level.setAttribute('aria-label', 'Access');
142
+ for (const [value, label] of [
143
+ ['read', 'can view'],
144
+ ['write', 'can edit']
145
+ ]) {
146
+ const option = element('option', '', label);
147
+ option.value = value;
148
+ this._level.appendChild(option);
149
+ }
150
+ personRow.append(this._email, this._level);
151
+ this.node.appendChild(personRow);
152
+ this.node.appendChild(
153
+ element(
154
+ 'p',
155
+ 'jp-OceanumShare-hint',
156
+ // Not "new lines": JupyterLab's dialog swallows Enter before it reaches the
157
+ // textarea, so one cannot be typed here. Pasted ones still separate addresses.
158
+ 'Separate several addresses with commas or spaces. Everyone you add gets the access chosen here.'
159
+ )
160
+ );
161
+
162
+ this._everyone = this._radio(name, 'Everyone with the link', false);
163
+ this.node.appendChild(
164
+ element('p', 'jp-OceanumShare-warning', EVERYONE_WARNING)
165
+ );
166
+ this._stopEveryone = this._radio(name, 'Stop sharing with everyone', false);
167
+
168
+ const update = () => {
169
+ this._email.disabled = !this._person.checked;
170
+ this._level.disabled = !this._person.checked;
171
+ };
172
+ for (const radio of [this._person, this._everyone, this._stopEveryone]) {
173
+ radio.addEventListener('change', update);
174
+ }
175
+ }
176
+
177
+ getValue(): ShareChoice | null {
178
+ if (this._everyone.checked) {
179
+ return { action: 'grant', grants: [PUBLIC_GRANT], invalid: [] };
180
+ }
181
+ if (this._stopEveryone.checked) {
182
+ return { action: 'revoke', grant: PUBLIC_GRANT };
183
+ }
184
+ const { emails, invalid } = parseShareEmails(this._email.value);
185
+ if (emails.length === 0 && invalid.length === 0) {
186
+ return null;
187
+ }
188
+ const permission = this._level.value === 'write' ? 'write' : 'read';
189
+ return {
190
+ action: 'grant',
191
+ grants: emails.map(entity => ({ type: 'user', entity, permission })),
192
+ invalid
193
+ };
194
+ }
195
+
196
+ private _radio(
197
+ name: string,
198
+ text: string,
199
+ checked: boolean
200
+ ): HTMLInputElement {
201
+ const label = element('label', 'jp-OceanumShare-choice');
202
+ const input = element('input', 'jp-OceanumShare-radio');
203
+ input.type = 'radio';
204
+ input.name = name;
205
+ input.checked = checked;
206
+ label.append(input, document.createTextNode(` ${text}`));
207
+ this.node.appendChild(label);
208
+ return input;
209
+ }
210
+
211
+ private static _count = 0;
212
+ private readonly _person: HTMLInputElement;
213
+ private readonly _everyone: HTMLInputElement;
214
+ private readonly _stopEveryone: HTMLInputElement;
215
+ private readonly _email: HTMLTextAreaElement;
216
+ private readonly _level: HTMLSelectElement;
217
+ }
package/style/index.css CHANGED
@@ -43,17 +43,26 @@
43
43
  color: var(--jp-ui-font-color1);
44
44
  }
45
45
 
46
+ /* Headers within the panel's tabs (Datamesh Workspace and the workspace name, Oceanum
47
+ * AI, My notebooks / Shared with me) share one text style: bold, standard case. */
48
+ .datamesh-workspace-header,
49
+ .datamesh-workspace-name,
50
+ .oceanum-ai-chat-header,
51
+ .oceanum-notebooks-heading {
52
+ font-weight: 600;
53
+ font-size: var(--jp-ui-font-size1);
54
+ color: var(--jp-ui-font-color1);
55
+ letter-spacing: 0.5px;
56
+ text-transform: none;
57
+ }
58
+
46
59
  /* Datamesh Workspace section header */
47
60
  .datamesh-workspace-header {
48
61
  display: flex;
49
62
  align-items: center;
50
63
  justify-content: space-between;
51
64
  padding: 8px 12px;
52
- font-weight: 600;
53
- font-size: var(--jp-ui-font-size0);
54
- color: var(--jp-ui-font-color2);
55
65
  background: var(--jp-layout-color1);
56
- letter-spacing: 0.5px;
57
66
  }
58
67
 
59
68
  .add-connection {
@@ -78,13 +87,9 @@
78
87
 
79
88
  .datamesh-workspace-name {
80
89
  flex: 0 0 auto;
81
- font-weight: 600;
82
- text-transform: uppercase;
83
- letter-spacing: 1px;
84
90
  padding: 8px 8px 8px 12px;
85
91
  margin: 0;
86
92
  margin-bottom: 10px;
87
- font-size: var(--jp-ui-font-size1);
88
93
  }
89
94
 
90
95
  .datasource-item-expand,
@@ -224,12 +229,8 @@
224
229
  display: flex;
225
230
  align-items: center;
226
231
  gap: 8px;
227
- font-weight: bold;
228
- font-size: var(--jp-ui-font-size1);
229
232
  padding: 6px 10px;
230
- color: var(--jp-ui-font-color1);
231
233
  background: var(--jp-layout-color2);
232
- letter-spacing: 0.5px;
233
234
  }
234
235
 
235
236
  .oceanum-ai-chat-title {
@@ -257,8 +258,13 @@
257
258
  white-space: nowrap;
258
259
  }
259
260
 
261
+ /* The thread takes only the height it needs, so the input sits under the last message
262
+ * rather than at the foot of the tab with a gap between them. `min-height: 0` is what
263
+ * lets it shrink past its content once the thread is taller than the space there is:
264
+ * without it a flex item refuses to, and the input would be pushed out of the panel. */
260
265
  .oceanum-ai-chat-messages {
261
- flex: 1 1 auto;
266
+ flex: 0 1 auto;
267
+ min-height: 0;
262
268
  overflow-y: auto;
263
269
  padding: 6px 8px;
264
270
  display: flex;
@@ -586,9 +592,6 @@
586
592
  display: flex;
587
593
  justify-content: space-between;
588
594
  padding: 6px 8px 2px;
589
- color: var(--jp-ui-font-color2);
590
- font-size: var(--jp-ui-font-size0);
591
- text-transform: uppercase;
592
595
  }
593
596
 
594
597
  .oceanum-notebooks-list {
@@ -597,7 +600,8 @@
597
600
  list-style: none;
598
601
  }
599
602
 
600
- .oceanum-notebooks-item {
603
+ .oceanum-notebooks-item,
604
+ .oceanum-notebooks-example {
601
605
  display: block;
602
606
  width: 100%;
603
607
  padding: 4px 8px;
@@ -610,7 +614,8 @@
610
614
  cursor: pointer;
611
615
  }
612
616
 
613
- .oceanum-notebooks-item:hover {
617
+ .oceanum-notebooks-item:hover,
618
+ .oceanum-notebooks-example:hover {
614
619
  background: var(--jp-layout-color2);
615
620
  }
616
621
 
@@ -618,10 +623,107 @@
618
623
  cursor: default;
619
624
  }
620
625
 
626
+ .oceanum-notebooks-actions {
627
+ padding: 6px 8px 2px;
628
+ }
629
+
630
+ .oceanum-notebooks-upload {
631
+ padding: 3px 8px;
632
+ border: var(--jp-border-width) solid var(--jp-border-color1);
633
+ border-radius: var(--jp-border-radius);
634
+ background: var(--jp-layout-color1);
635
+ color: var(--jp-ui-font-color1);
636
+ font-family: var(--jp-ui-font-family);
637
+ font-size: var(--jp-ui-font-size1);
638
+ cursor: pointer;
639
+ }
640
+
641
+ .oceanum-notebooks-upload:hover {
642
+ background: var(--jp-layout-color2);
643
+ }
644
+
621
645
  .oceanum-notebooks-item-static:hover {
622
646
  background: transparent;
623
647
  }
624
648
 
649
+ /* The Examples heading's count and show/hide switch, kept together on the right. */
650
+ .oceanum-notebooks-heading-end {
651
+ display: flex;
652
+ align-items: center;
653
+ gap: 6px;
654
+ }
655
+
656
+ /* The Examples show/hide switch (a button with role="switch"). */
657
+ .oceanum-notebooks-switch {
658
+ position: relative;
659
+ flex: none;
660
+ width: 24px;
661
+ height: 14px;
662
+ padding: 0;
663
+ border: none;
664
+ border-radius: 7px;
665
+ background: var(--jp-layout-color3);
666
+ cursor: pointer;
667
+ }
668
+
669
+ .oceanum-notebooks-switch[aria-checked='true'] {
670
+ background: var(--jp-brand-color1);
671
+ }
672
+
673
+ .oceanum-notebooks-switch:focus-visible {
674
+ outline: 1px solid var(--jp-brand-color1);
675
+ outline-offset: 1px;
676
+ }
677
+
678
+ .oceanum-notebooks-switch-thumb {
679
+ position: absolute;
680
+ top: 2px;
681
+ left: 2px;
682
+ width: 10px;
683
+ height: 10px;
684
+ border-radius: 50%;
685
+ background: var(--jp-layout-color0);
686
+ transition: left 0.1s ease;
687
+ }
688
+
689
+ .oceanum-notebooks-switch[aria-checked='true'] .oceanum-notebooks-switch-thumb {
690
+ left: 12px;
691
+ }
692
+
693
+ /* Forced colours replace backgrounds, which would leave a switch drawn only with them
694
+ invisible: draw it with system colours instead. */
695
+ @media (forced-colors: active) {
696
+ .oceanum-notebooks-switch {
697
+ forced-color-adjust: none;
698
+ box-sizing: border-box;
699
+ border: 1px solid ButtonText;
700
+ background: Canvas;
701
+ }
702
+
703
+ .oceanum-notebooks-switch[aria-checked='true'] {
704
+ background: Highlight;
705
+ }
706
+
707
+ .oceanum-notebooks-switch-thumb {
708
+ top: 1px;
709
+ left: 1px;
710
+ background: ButtonText;
711
+ }
712
+
713
+ .oceanum-notebooks-switch[aria-checked='true']
714
+ .oceanum-notebooks-switch-thumb {
715
+ left: 11px;
716
+ background: HighlightText;
717
+ }
718
+ }
719
+
720
+ /* A group within Examples: quieter than the tab's section headings. */
721
+ .oceanum-notebooks-subheading {
722
+ padding: 4px 8px 0;
723
+ color: var(--jp-ui-font-color2);
724
+ font-size: var(--jp-ui-font-size0);
725
+ }
726
+
625
727
  .oceanum-notebooks-name {
626
728
  overflow: hidden;
627
729
  text-overflow: ellipsis;
@@ -633,3 +735,95 @@
633
735
  color: var(--jp-error-color1);
634
736
  font-size: var(--jp-ui-font-size1);
635
737
  }
738
+
739
+ /* Open from Oceanum and Share on Oceanum dialogs (src/share/widgets.ts). */
740
+ .jp-OceanumShare-body {
741
+ min-width: 320px;
742
+ max-width: 560px;
743
+ }
744
+
745
+ .jp-OceanumShare-heading {
746
+ margin: 12px 0 6px;
747
+ font-size: var(--jp-ui-font-size1);
748
+ font-weight: 600;
749
+ }
750
+
751
+ .jp-OceanumShare-heading:first-child {
752
+ margin-top: 0;
753
+ }
754
+
755
+ .jp-OceanumShare-empty,
756
+ .jp-OceanumShare-hint {
757
+ margin: 0 0 6px;
758
+ color: var(--jp-ui-font-color2);
759
+ }
760
+
761
+ .jp-OceanumShare-list {
762
+ max-height: 240px;
763
+ margin: 0;
764
+ padding: 0;
765
+ overflow-y: auto;
766
+ list-style: none;
767
+ border: var(--jp-border-width) solid var(--jp-border-color1);
768
+ }
769
+
770
+ .jp-OceanumShare-item {
771
+ display: flex;
772
+ flex-direction: column;
773
+ padding: 6px 8px;
774
+ cursor: pointer;
775
+ }
776
+
777
+ .jp-OceanumShare-item:hover {
778
+ background: var(--jp-layout-color2);
779
+ }
780
+
781
+ .jp-OceanumShare-item.jp-mod-selected {
782
+ background: var(--jp-brand-color3);
783
+ }
784
+
785
+ .jp-OceanumShare-meta {
786
+ color: var(--jp-ui-font-color2);
787
+ font-size: var(--jp-ui-font-size0);
788
+ }
789
+
790
+ .jp-OceanumShare-linkRow,
791
+ .jp-OceanumShare-personRow {
792
+ display: flex;
793
+ gap: 6px;
794
+ align-items: center;
795
+ margin-bottom: 6px;
796
+ }
797
+
798
+ .jp-OceanumShare-linkRow > :first-child,
799
+ .jp-OceanumShare-personRow > :first-child {
800
+ flex: 1;
801
+ }
802
+
803
+ /* A textarea so several addresses fit; without this it would default to monospace. */
804
+ .jp-OceanumShare-email {
805
+ font: inherit;
806
+ resize: vertical;
807
+ }
808
+
809
+ /* Keep the access select beside the first line of addresses, not centred against them. */
810
+ .jp-OceanumShare-personRow {
811
+ align-items: flex-start;
812
+ }
813
+
814
+ .jp-OceanumShare-choice {
815
+ display: block;
816
+ margin: 6px 0;
817
+ }
818
+
819
+ .jp-OceanumShare-choice input[type='radio'] {
820
+ width: auto;
821
+ height: auto;
822
+ margin: 0 4px 0 0;
823
+ appearance: auto;
824
+ }
825
+
826
+ .jp-OceanumShare-warning {
827
+ margin: 0 0 0 20px;
828
+ color: var(--jp-warn-color0);
829
+ }
@@ -1,37 +0,0 @@
1
- /**
2
- * Reading the Oceanum spec store's `notebook` records.
3
- *
4
- * Deliberately small: only what the Stored notebooks tab needs. oceanum-notebook's
5
- * share-oceanum package has a fuller client (create, update, permissions), and when
6
- * sharing moves here the two should become one rather than sitting side by side.
7
- */
8
- /** A spec store list entry. The list omits the notebook body. */
9
- export interface ISpecSummary {
10
- id: string;
11
- name: string;
12
- description: string | null;
13
- /** Set when the record is created and never updated, so this is a creation time. */
14
- modified: string;
15
- /** Null unless the caller created the record: the store hides other creators. */
16
- creator: string | null;
17
- }
18
- /**
19
- * Every notebook the signed-in user may read: their own and those shared with them.
20
- *
21
- * The store answers an unauthenticated caller with an empty list rather than an error,
22
- * so a missing token is indistinguishable from an empty account — callers should check
23
- * they are signed in before showing "no notebooks".
24
- */
25
- export declare function listNotebooks(specsUrl: string, token: string, fetcher?: typeof fetch): Promise<ISpecSummary[]>;
26
- /**
27
- * Split a listing into the caller's own notebooks and the rest, newest first.
28
- *
29
- * The store only sends `creator` on records the caller created, so anything else it
30
- * returns is shared with them — directly, or because it is public. Addresses are
31
- * compared case-insensitively: the store matches entities as case-sensitive globs, so
32
- * the two sides can differ in case.
33
- */
34
- export declare function partitionNotebooks(items: readonly ISpecSummary[], email: string | null): {
35
- mine: ISpecSummary[];
36
- shared: ISpecSummary[];
37
- };
package/lib/specStore.js DELETED
@@ -1,57 +0,0 @@
1
- /**
2
- * Reading the Oceanum spec store's `notebook` records.
3
- *
4
- * Deliberately small: only what the Stored notebooks tab needs. oceanum-notebook's
5
- * share-oceanum package has a fuller client (create, update, permissions), and when
6
- * sharing moves here the two should become one rather than sitting side by side.
7
- */
8
- function isSummary(value) {
9
- if (typeof value !== 'object' || value === null) {
10
- return false;
11
- }
12
- const v = value;
13
- return (typeof v.id === 'string' &&
14
- typeof v.name === 'string' &&
15
- typeof v.modified === 'string');
16
- }
17
- /**
18
- * Every notebook the signed-in user may read: their own and those shared with them.
19
- *
20
- * The store answers an unauthenticated caller with an empty list rather than an error,
21
- * so a missing token is indistinguishable from an empty account — callers should check
22
- * they are signed in before showing "no notebooks".
23
- */
24
- export async function listNotebooks(specsUrl, token, fetcher = fetch) {
25
- const url = `${specsUrl.replace(/\/+$/, '')}/specs/notebook`;
26
- const response = await fetcher(url, {
27
- headers: { Authorization: `Bearer ${token}` }
28
- });
29
- if (!response.ok) {
30
- // The store answers a bad or expired token with 400, and 403 both for no access
31
- // and for a record that does not exist.
32
- throw new Error(`Oceanum.io returned HTTP ${response.status}.`);
33
- }
34
- const data = await response.json();
35
- if (!Array.isArray(data)) {
36
- throw new Error('Oceanum.io returned an unexpected response.');
37
- }
38
- return data.filter(isSummary);
39
- }
40
- /**
41
- * Split a listing into the caller's own notebooks and the rest, newest first.
42
- *
43
- * The store only sends `creator` on records the caller created, so anything else it
44
- * returns is shared with them — directly, or because it is public. Addresses are
45
- * compared case-insensitively: the store matches entities as case-sensitive globs, so
46
- * the two sides can differ in case.
47
- */
48
- export function partitionNotebooks(items, email) {
49
- var _a;
50
- const me = (_a = email === null || email === void 0 ? void 0 : email.toLowerCase()) !== null && _a !== void 0 ? _a : null;
51
- const sorted = [...items].sort((a, b) => b.modified.localeCompare(a.modified));
52
- const isMine = (item) => { var _a; return me !== null && ((_a = item.creator) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === me; };
53
- return {
54
- mine: sorted.filter(isMine),
55
- shared: sorted.filter(item => !isMine(item))
56
- };
57
- }