bookish-press 0.4.16 → 0.4.21

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.
@@ -1,8 +1,7 @@
1
1
  <script>import Parser from '../../models/chapter/Parser';
2
- import { createNewEdition, publishEdition } from '../../models/CRUD';
2
+ import { createNewEdition, publish } from '../../models/CRUD';
3
3
  import Format from '../chapter/Format.svelte';
4
4
  import BookishEditor from '../editor/BookishEditor.svelte';
5
- import Switch from '../editor/Switch.svelte';
6
5
  import Instructions from './Instructions.svelte';
7
6
  import Note from '../editor/Note.svelte';
8
7
  import { getBook, getEdition, isEditable } from './Contexts';
@@ -14,22 +13,28 @@ let edition = getEdition();
14
13
  let editable = isEditable();
15
14
  let book = getBook();
16
15
  // Book revisions
17
- $: bookRevisions = $book?.getRevisions();
18
- // Copy the current draft to create a new draft.
19
- function handlePublish(index, published) {
20
- if ($book === undefined)
21
- return;
22
- book.set($book.asPublished(published, index));
23
- }
16
+ $: bookEditions = $book?.getEditions();
17
+ let publishing = new Map();
24
18
  async function handleDraftEdition() {
25
19
  if ($book === undefined)
26
20
  return;
27
21
  book.set(await createNewEdition($book));
28
22
  }
23
+ async function handlePublish(index) {
24
+ if ($book) {
25
+ publishing.set(index, true);
26
+ const error = await publish($book, index);
27
+ if (error)
28
+ publishing.set(index, error);
29
+ else
30
+ publishing.delete(index);
31
+ publishing = new Map(publishing);
32
+ }
33
+ }
29
34
  </script>
30
35
 
31
36
  {#if $edition}
32
- {#if $book && bookRevisions}
37
+ {#if $book && bookEditions}
33
38
  <PageHeader id="revisions"
34
39
  >Editions {#if editable}<Button
35
40
  tooltip="Create a new edition"
@@ -44,11 +49,11 @@ async function handleDraftEdition() {
44
49
  they explicitly choose to view a previous edition.
45
50
  </Instructions>
46
51
  <Rows>
47
- {#each bookRevisions as revision, index (revision.ref)}
52
+ {#each bookEditions as bookEdition, index (bookEdition.ref)}
48
53
  {@const editionNumber =
49
- bookRevisions === undefined
54
+ bookEditions === undefined
50
55
  ? -1
51
- : bookRevisions.length - index}
56
+ : bookEditions.length - index}
52
57
  {@const editionLabel =
53
58
  editionNumber +
54
59
  (editionNumber === 1
@@ -58,9 +63,9 @@ async function handleDraftEdition() {
58
63
  : editionNumber === 3
59
64
  ? 'rd'
60
65
  : 'th')}
61
- {@const viewing = revision.ref.id === $edition.getRef()?.id}
66
+ {@const viewing = bookEdition.ref.id === $edition.getRef()?.id}
62
67
 
63
- {#if $book && (editable || revision.published)}
68
+ {#if $book && (editable || bookEdition.published)}
64
69
  <tr>
65
70
  <td>
66
71
  {#if viewing}
@@ -68,12 +73,12 @@ async function handleDraftEdition() {
68
73
  {:else}
69
74
  <Link
70
75
  to={`/write/${$book.ref.id}/${
71
- bookRevisions.length - index
76
+ bookEditions.length - index
72
77
  }`}>{editionLabel}</Link
73
78
  >
74
79
  {/if}
75
80
  <Note
76
- >{new Date(revision.time).toLocaleDateString(
81
+ >{new Date(bookEdition.time).toLocaleDateString(
77
82
  'en-us'
78
83
  )}</Note
79
84
  >
@@ -83,7 +88,7 @@ async function handleDraftEdition() {
83
88
  <BookishEditor
84
89
  ast={Parser.parseFormat(
85
90
  undefined,
86
- revision.summary
91
+ bookEdition.summary
87
92
  ).withTextIfEmpty()}
88
93
  save={(newSummary) =>
89
94
  $book
@@ -98,19 +103,19 @@ async function handleDraftEdition() {
98
103
  component={Format}
99
104
  placeholder="Summarize this edition's changes."
100
105
  />
101
- {:else if revision.summary === ''}
106
+ {:else if bookEdition.summary === ''}
102
107
  <em>No summary of changes</em>
103
108
  {:else}
104
- {revision.summary}
109
+ {bookEdition.summary}
105
110
  {/if}
106
111
  </td>
107
112
  <td style="whitespace: nowrap">
108
113
  {#if editable}
109
- <Switch
114
+ <!-- <Switch
110
115
  options={['hidden', 'published']}
111
- enabled={revision.published ||
112
- revision.summary !== ''}
113
- value={revision.published
116
+ enabled={bookEdition.published ||
117
+ bookEdition.summary !== ''}
118
+ value={bookEdition.published
114
119
  ? 'published'
115
120
  : 'hidden'}
116
121
  edit={(published) =>
@@ -118,7 +123,16 @@ async function handleDraftEdition() {
118
123
  index,
119
124
  published === 'published'
120
125
  )}
121
- />
126
+ /> -->
127
+ <Button
128
+ tooltip="publish edition"
129
+ command={() => handlePublish(index)}
130
+ disabled={publishing.has(index)}
131
+ >publish</Button
132
+ >
133
+ {#if typeof publishing.get(index) === 'string'}
134
+ {publishing.get(index)}
135
+ {/if}
122
136
  {/if}
123
137
  </td>
124
138
  </tr>
package/models/CRUD.d.ts CHANGED
@@ -9,9 +9,9 @@ export declare function readBook(bookID: string): Promise<Book | null>;
9
9
  export declare function readEdition(editionID: string): Promise<Edition>;
10
10
  export declare const createBook: (userID: string) => Promise<string>;
11
11
  export declare function createNewEdition(book: Book): Promise<Book>;
12
- export declare function publishEdition(book: Book, index: number, published: boolean): Promise<void>;
13
12
  export declare const updateEdition: (previousEdition: Edition | undefined, newEdition: Edition) => Promise<Edition>;
14
13
  export declare function updateBook(book: Book): Promise<void>;
15
14
  export declare function readChapterText(chapterRef: DocumentReference): Promise<ChapterContent>;
16
15
  export declare function isSubdomainAvailable(subdomain: string, book: Book): Promise<boolean>;
17
16
  export declare function getBookIDFromName(subdomain: string): Promise<string | null>;
17
+ export declare function publish(book: Book, index: number): Promise<string | undefined>;
package/models/CRUD.js CHANGED
@@ -1,5 +1,6 @@
1
- import { db } from './Firebase';
1
+ import { db, functions } from './Firebase';
2
2
  import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc, DocumentReference, } from 'firebase/firestore';
3
+ import { getFunctions, httpsCallable } from 'firebase/functions';
3
4
  import Edition from './book/Edition';
4
5
  import Book from './book/Book';
5
6
  export async function getPublishedBooks() {
@@ -77,7 +78,7 @@ export const createBook = async (userID) => {
77
78
  authors: [],
78
79
  description: '',
79
80
  cover: null,
80
- revisions: [
81
+ editions: [
81
82
  {
82
83
  ref: editionRef,
83
84
  time: Date.now(),
@@ -108,7 +109,7 @@ export async function createNewEdition(book) {
108
109
  // Update the edition, creating chapter text as necessary.
109
110
  updateEdition(undefined, newEdition.withRef(newEditionRef));
110
111
  // Put the new edition in the book.
111
- const revisions = book.getRevisions();
112
+ const revisions = book.getEditions();
112
113
  revisions.unshift({
113
114
  ref: newEditionRef,
114
115
  time: Date.now(),
@@ -120,14 +121,6 @@ export async function createNewEdition(book) {
120
121
  updateBook(book);
121
122
  return book;
122
123
  }
123
- export async function publishEdition(book, index, published) {
124
- if (!db)
125
- throw Error("Can't publish draft, no Firebase connection.");
126
- // Update the edition's published status.
127
- book.asPublished(published, index);
128
- // Return the revised Book for viewing
129
- updateBook(book);
130
- }
131
124
  export const updateEdition = async (previousEdition, newEdition) => {
132
125
  if (!db)
133
126
  throw Error("Can't update edition, not connected to Firebase.");
@@ -192,3 +185,23 @@ export async function getBookIDFromName(subdomain) {
192
185
  else
193
186
  return null;
194
187
  }
188
+ export async function publish(book, index) {
189
+ if (!db)
190
+ throw Error("Can't publish, no Firebase connection.");
191
+ if (!functions)
192
+ throw Error("Can't publish, no connection to Firebase functions.");
193
+ const edition = await book.getEditionNumber(index + 1);
194
+ if (edition === undefined)
195
+ return `Couldn't find edition ${index} number to publish`;
196
+ const editionRef = edition.getRef();
197
+ if (editionRef === undefined)
198
+ return `Couldn't find edition to publish`;
199
+ const publishEdition = httpsCallable(functions, 'publishEdition');
200
+ const result = await publishEdition({
201
+ book: book.getRef().id,
202
+ edition: editionRef.id,
203
+ });
204
+ return result.data;
205
+ // Update the edition's published status in the book.
206
+ // await updateBook(book.withEditionAsPublished(true, index));
207
+ }
@@ -2,3 +2,4 @@ export declare const app: import("@firebase/app").FirebaseApp | undefined;
2
2
  export declare const auth: import("@firebase/auth").Auth | undefined;
3
3
  export declare const db: import("@firebase/firestore").Firestore | undefined;
4
4
  export declare const storage: import("@firebase/storage").FirebaseStorage | undefined;
5
+ export declare const functions: import("@firebase/functions").Functions | undefined;
@@ -1,6 +1,7 @@
1
1
  import { initializeApp } from 'firebase/app';
2
2
  import { connectAuthEmulator, getAuth } from 'firebase/auth';
3
3
  import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore';
4
+ import { connectFunctionsEmulator, getFunctions } from 'firebase/functions';
4
5
  import { connectStorageEmulator, getStorage } from 'firebase/storage';
5
6
  const isReader = import.meta.env.VITE_READER === 'true';
6
7
  // Initialize Firebase using the environment variables provided at build time.
@@ -18,6 +19,7 @@ export const app = isReader
18
19
  export const auth = isReader ? undefined : getAuth();
19
20
  export const db = isReader ? undefined : getFirestore();
20
21
  export const storage = isReader ? undefined : getStorage();
22
+ export const functions = isReader ? undefined : getFunctions();
21
23
  if (!isReader && import.meta.env.DEV) {
22
24
  if (db)
23
25
  connectFirestoreEmulator(db, 'localhost', 8080);
@@ -25,4 +27,6 @@ if (!isReader && import.meta.env.DEV) {
25
27
  connectAuthEmulator(auth, 'http://localhost:9099');
26
28
  if (storage)
27
29
  connectStorageEmulator(storage, 'localhost', 9199);
30
+ if (functions)
31
+ connectFunctionsEmulator(functions, 'localhost', 5001);
28
32
  }
@@ -1,7 +1,7 @@
1
1
  import type { DocumentReference } from 'firebase/firestore';
2
2
  import BookMedia from './BookMedia';
3
3
  import type Edition from './Edition';
4
- export type Revision = {
4
+ export type EditionInfo = {
5
5
  ref: DocumentReference;
6
6
  time: number;
7
7
  summary: string;
@@ -12,7 +12,7 @@ export type BookSpecification = {
12
12
  authors: string[];
13
13
  description: string;
14
14
  cover: string | null;
15
- revisions: Revision[];
15
+ editions: EditionInfo[];
16
16
  domain?: string;
17
17
  uids: string[];
18
18
  };
@@ -22,11 +22,11 @@ export default class Book {
22
22
  readonly authors: string[];
23
23
  readonly description: string;
24
24
  readonly cover: string | null;
25
- readonly revisions: Revision[];
25
+ readonly editions: EditionInfo[];
26
26
  readonly domain: string | undefined;
27
27
  readonly uids: string[];
28
28
  media: BookMedia;
29
- constructor(ref: DocumentReference, title: string, authors: string[], description: string, cover: string | null, revisions: Revision[], domain: string | undefined, uids: string[]);
29
+ constructor(ref: DocumentReference, title: string, authors: string[], description: string, cover: string | null, revisions: EditionInfo[], domain: string | undefined, uids: string[]);
30
30
  getMedia(): BookMedia;
31
31
  static fromJSON(ref: DocumentReference, book: BookSpecification): Book;
32
32
  toJSON(): BookSpecification;
@@ -48,9 +48,9 @@ export default class Book {
48
48
  getLatestPublishedEdition(): undefined | Promise<Edition>;
49
49
  getLatestPublishedEditionID(): string | undefined;
50
50
  getLatestEditionID(): string | undefined;
51
- getRevisions(): Revision[];
52
- withRevisions(revisions: Revision[]): Book;
51
+ getEditions(): EditionInfo[];
52
+ withRevisions(revisions: EditionInfo[]): Book;
53
53
  withEditionSummary(summary: string, index: number): Book;
54
- asPublished(published: boolean, index: number): Book;
54
+ withEditionAsPublished(published: boolean, editionNumber: number): Book;
55
55
  hasPublishedEdition(): boolean;
56
56
  }
@@ -1,4 +1,4 @@
1
- import { readEdition, updateBook } from '../CRUD';
1
+ import { readEdition } from '../CRUD';
2
2
  import BookMedia from './BookMedia';
3
3
  export default class Book {
4
4
  ref;
@@ -6,7 +6,7 @@ export default class Book {
6
6
  authors;
7
7
  description;
8
8
  cover;
9
- revisions;
9
+ editions;
10
10
  domain;
11
11
  uids;
12
12
  media;
@@ -16,7 +16,7 @@ export default class Book {
16
16
  this.authors = authors;
17
17
  this.description = description;
18
18
  this.cover = cover;
19
- this.revisions = revisions;
19
+ this.editions = revisions;
20
20
  this.domain = domain;
21
21
  this.uids = uids;
22
22
  this.media = new BookMedia(this);
@@ -25,7 +25,7 @@ export default class Book {
25
25
  return this.media;
26
26
  }
27
27
  static fromJSON(ref, book) {
28
- return new Book(ref, book.title, book.authors, book.description, book.cover, book.revisions, book.domain, book.uids);
28
+ return new Book(ref, book.title, book.authors, book.description, book.cover, book.editions, book.domain, book.uids);
29
29
  }
30
30
  toJSON() {
31
31
  const json = {
@@ -33,7 +33,7 @@ export default class Book {
33
33
  authors: this.authors,
34
34
  description: this.description,
35
35
  cover: this.cover,
36
- revisions: this.revisions,
36
+ editions: this.editions,
37
37
  uids: this.uids,
38
38
  };
39
39
  if (this.domain !== undefined)
@@ -44,31 +44,31 @@ export default class Book {
44
44
  return this.domain;
45
45
  }
46
46
  withSubdomain(domain) {
47
- return new Book(this.ref, this.title, this.authors, this.description, this.cover, this.revisions, domain.length === 0 ? undefined : domain, this.uids);
47
+ return new Book(this.ref, this.title, this.authors, this.description, this.cover, this.editions, domain.length === 0 ? undefined : domain, this.uids);
48
48
  }
49
49
  getTitle() {
50
50
  return this.title;
51
51
  }
52
52
  withTitle(title) {
53
- return new Book(this.ref, title, this.authors, this.description, this.cover, this.revisions, this.domain, this.uids);
53
+ return new Book(this.ref, title, this.authors, this.description, this.cover, this.editions, this.domain, this.uids);
54
54
  }
55
55
  getDescription() {
56
56
  return this.description;
57
57
  }
58
58
  withDescription(description) {
59
- return new Book(this.ref, this.title, this.authors, description, this.cover, this.revisions, this.domain, this.uids);
59
+ return new Book(this.ref, this.title, this.authors, description, this.cover, this.editions, this.domain, this.uids);
60
60
  }
61
61
  getCover() {
62
62
  return this.cover;
63
63
  }
64
64
  withCover(cover) {
65
- return new Book(this.ref, this.title, this.authors, this.description, cover, this.revisions, this.domain, this.uids);
65
+ return new Book(this.ref, this.title, this.authors, this.description, cover, this.editions, this.domain, this.uids);
66
66
  }
67
67
  getAuthors() {
68
68
  return this.authors;
69
69
  }
70
70
  withAuthors(authors) {
71
- return new Book(this.ref, this.title, authors, this.description, this.cover, this.revisions, this.domain, this.uids);
71
+ return new Book(this.ref, this.title, authors, this.description, this.cover, this.editions, this.domain, this.uids);
72
72
  }
73
73
  getRef() {
74
74
  return this.ref;
@@ -77,66 +77,66 @@ export default class Book {
77
77
  return this.ref.id;
78
78
  }
79
79
  getDraftEdition() {
80
- return this.revisions.length === 0
80
+ return this.editions.length === 0
81
81
  ? undefined
82
- : readEdition(this.revisions[0].ref.id);
82
+ : readEdition(this.editions[0].ref.id);
83
83
  }
84
84
  isValidEditionNumber(edition) {
85
85
  return this.getEditionNumber(edition) !== undefined;
86
86
  }
87
87
  /* Note: editions are numbered 1-n, but stored in reverse chronological order. */
88
88
  getEditionNumber(number) {
89
- return number < 1 || number > this.revisions.length
89
+ return number < 1 || number > this.editions.length
90
90
  ? undefined
91
- : readEdition(this.revisions[this.revisions.length - number].ref.id);
91
+ : readEdition(this.editions[this.editions.length - number].ref.id);
92
92
  }
93
93
  getLatestPublishedEdition() {
94
94
  const latest = this.getLatestPublishedEditionID();
95
95
  return latest === undefined ? undefined : readEdition(latest);
96
96
  }
97
97
  getLatestPublishedEditionID() {
98
- return this.revisions.find((e) => e.published)?.ref.id;
98
+ return this.editions.find((e) => e.published)?.ref.id;
99
99
  }
100
100
  getLatestEditionID() {
101
- return this.revisions[0]?.ref.id;
101
+ return this.editions[0]?.ref.id;
102
102
  }
103
- getRevisions() {
104
- return this.revisions.slice();
103
+ getEditions() {
104
+ return this.editions.slice();
105
105
  }
106
106
  withRevisions(revisions) {
107
107
  return new Book(this.ref, this.title, this.authors, this.description, this.cover, revisions, this.domain, this.uids);
108
108
  }
109
109
  withEditionSummary(summary, index) {
110
- if (index < 0 || index >= this.revisions.length)
110
+ if (index < 0 || index >= this.editions.length)
111
111
  return this;
112
- const revision = this.revisions[index];
112
+ const revision = this.editions[index];
113
113
  return this.withRevisions([
114
- ...this.revisions.slice(0, index),
114
+ ...this.editions.slice(0, index),
115
115
  {
116
116
  ref: revision.ref,
117
117
  time: revision.time,
118
118
  summary,
119
119
  published: revision.published,
120
120
  },
121
- ...this.revisions.slice(index + 1),
121
+ ...this.editions.slice(index + 1),
122
122
  ]);
123
123
  }
124
- asPublished(published, index) {
125
- if (index < 0 || index >= this.revisions.length)
124
+ withEditionAsPublished(published, editionNumber) {
125
+ if (editionNumber < 0 || editionNumber >= this.editions.length)
126
126
  return this;
127
- const revision = this.revisions[index];
127
+ const revision = this.editions[editionNumber];
128
128
  return this.withRevisions([
129
- ...this.revisions.slice(0, index),
129
+ ...this.editions.slice(0, editionNumber),
130
130
  {
131
131
  ref: revision.ref,
132
132
  time: revision.time,
133
133
  summary: revision.summary,
134
134
  published,
135
135
  },
136
- ...this.revisions.slice(index + 1),
136
+ ...this.editions.slice(editionNumber + 1),
137
137
  ]);
138
138
  }
139
139
  hasPublishedEdition() {
140
- return (this.revisions.find((revision) => revision.published) !== undefined);
140
+ return (this.editions.find((revision) => revision.published) !== undefined);
141
141
  }
142
142
  }
@@ -42,7 +42,7 @@ export default class Edition {
42
42
  readonly glossary: Record<string, Definition>;
43
43
  readonly theme: Theme | null;
44
44
  constructor(bookRef: DocumentReference | undefined, editionRef: DocumentReference | undefined, uids: string[], title: string, authors: string[], images: Record<string, string | null>, description: string, chapters: Chapter[], license: string, acknowledgements: string, tags: string[], sources: Record<string, string>, references: Record<string, string | string[]> | Record<string, FormatNode | Reference>, symbols: Record<string, string>, glossary: Record<string, Definition>, theme: Theme | null);
45
- static fromJSON(editionRef: DocumentReference | undefined, spec: EditionSpecification): Promise<Edition>;
45
+ static fromJSON(editionRef: DocumentReference | undefined, spec: EditionSpecification): Edition;
46
46
  static parseReference(citationID: string, ref: string | string[], edition: Edition, short?: boolean): FormatNode | Reference;
47
47
  toObject(): EditionSpecification;
48
48
  getRef(): DocumentReference<import("@firebase/firestore").DocumentData> | undefined;
@@ -46,7 +46,7 @@ export default class Edition {
46
46
  this.glossary = Object.assign({}, glossary);
47
47
  this.theme = theme;
48
48
  }
49
- static async fromJSON(editionRef, spec) {
49
+ static fromJSON(editionRef, spec) {
50
50
  // Copy all of the specification metadata to fields.
51
51
  // Choose suitable defaults if the spec is lacking a field.
52
52
  return new Edition(spec.bookRef, editionRef, spec.uids, spec.title, spec.authors, spec.images, spec.description, spec.chapters.map((chap) => Chapter.fromJSON(chap, '')), spec.license, spec.acknowledgements, spec.tags, spec.sources, spec.references, spec.symbols, spec.glossary, spec.theme);
@@ -533,7 +533,7 @@ export default class Edition {
533
533
  }
534
534
  /* Edition numbers are 1 to N */
535
535
  getEditionNumber(book) {
536
- const revisions = book.getRevisions();
536
+ const revisions = book.getEditions();
537
537
  if (revisions === undefined)
538
538
  return undefined;
539
539
  for (let i = 0; i < revisions.length; i++)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bookish-press",
3
3
  "private": false,
4
- "version": "0.4.16",
4
+ "version": "0.4.21",
5
5
  "type": "module",
6
6
  "description": "A platform for reading and writing readable, accessible, beautiful online books.",
7
7
  "license": "MIT",