@geekity/cms 0.34.0 → 0.35.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 (192) hide show
  1. package/admin/pages/documents/chooser.njk +30 -0
  2. package/admin/pages/documents/editor.njk +394 -164
  3. package/admin/pages/media/file.njk +142 -0
  4. package/admin/pages/media/library.njk +103 -198
  5. package/admin/pages/settings/general.njk +48 -1
  6. package/admin/pages/settings/privacy.njk +1 -1
  7. package/admin/pages/tools/maintenance.njk +48 -0
  8. package/admin/src/admin.css +1 -0
  9. package/admin/static/admin.css +728 -65
  10. package/admin/static/copy.js +30 -23
  11. package/admin/static/item-lookup.js +179 -0
  12. package/admin/static/media.js +82 -0
  13. package/dist/admin/documents.d.ts.map +1 -1
  14. package/dist/admin/documents.js +141 -22
  15. package/dist/admin/documents.js.map +1 -1
  16. package/dist/admin/editor-actions.d.ts.map +1 -1
  17. package/dist/admin/editor-actions.js +2 -3
  18. package/dist/admin/editor-actions.js.map +1 -1
  19. package/dist/admin/editor-fields.d.ts +20 -8
  20. package/dist/admin/editor-fields.d.ts.map +1 -1
  21. package/dist/admin/editor-fields.js +35 -12
  22. package/dist/admin/editor-fields.js.map +1 -1
  23. package/dist/admin/editor-kinds.d.ts +30 -0
  24. package/dist/admin/editor-kinds.d.ts.map +1 -0
  25. package/dist/admin/editor-kinds.js +136 -0
  26. package/dist/admin/editor-kinds.js.map +1 -0
  27. package/dist/admin/editor-layout.d.ts +10 -1
  28. package/dist/admin/editor-layout.d.ts.map +1 -1
  29. package/dist/admin/editor-layout.js +38 -31
  30. package/dist/admin/editor-layout.js.map +1 -1
  31. package/dist/admin/federation.d.ts +1 -1
  32. package/dist/admin/federation.d.ts.map +1 -1
  33. package/dist/admin/federation.js +2 -1
  34. package/dist/admin/federation.js.map +1 -1
  35. package/dist/admin/formatting.d.ts +2 -0
  36. package/dist/admin/formatting.d.ts.map +1 -1
  37. package/dist/admin/formatting.js +8 -0
  38. package/dist/admin/formatting.js.map +1 -1
  39. package/dist/admin/index.d.ts +1 -1
  40. package/dist/admin/index.d.ts.map +1 -1
  41. package/dist/admin/index.js +1 -1
  42. package/dist/admin/index.js.map +1 -1
  43. package/dist/admin/item-lookup.d.ts +16 -0
  44. package/dist/admin/item-lookup.d.ts.map +1 -0
  45. package/dist/admin/item-lookup.js +192 -0
  46. package/dist/admin/item-lookup.js.map +1 -0
  47. package/dist/admin/maintenance.d.ts +2 -0
  48. package/dist/admin/maintenance.d.ts.map +1 -1
  49. package/dist/admin/maintenance.js +14 -0
  50. package/dist/admin/maintenance.js.map +1 -1
  51. package/dist/admin/media.d.ts +43 -29
  52. package/dist/admin/media.d.ts.map +1 -1
  53. package/dist/admin/media.js +221 -54
  54. package/dist/admin/media.js.map +1 -1
  55. package/dist/admin/preview.d.ts.map +1 -1
  56. package/dist/admin/preview.js +9 -12
  57. package/dist/admin/preview.js.map +1 -1
  58. package/dist/admin/routes.d.ts.map +1 -1
  59. package/dist/admin/routes.js +3 -0
  60. package/dist/admin/routes.js.map +1 -1
  61. package/dist/admin/settings-general.d.ts +6 -0
  62. package/dist/admin/settings-general.d.ts.map +1 -1
  63. package/dist/admin/settings-general.js +46 -1
  64. package/dist/admin/settings-general.js.map +1 -1
  65. package/dist/admin/templates.d.ts +2 -0
  66. package/dist/admin/templates.d.ts.map +1 -1
  67. package/dist/admin/templates.js +4 -0
  68. package/dist/admin/templates.js.map +1 -1
  69. package/dist/cli.d.ts.map +1 -1
  70. package/dist/cli.js +2 -1
  71. package/dist/cli.js.map +1 -1
  72. package/dist/cms/app.d.ts.map +1 -1
  73. package/dist/cms/app.js +1 -0
  74. package/dist/cms/app.js.map +1 -1
  75. package/dist/cms/services.d.ts +2 -0
  76. package/dist/cms/services.d.ts.map +1 -1
  77. package/dist/cms/services.js +13 -0
  78. package/dist/cms/services.js.map +1 -1
  79. package/dist/content/document-form.d.ts +5 -13
  80. package/dist/content/document-form.d.ts.map +1 -1
  81. package/dist/content/document-form.js +2 -3
  82. package/dist/content/document-form.js.map +1 -1
  83. package/dist/content/excerpt.d.ts +1 -3
  84. package/dist/content/excerpt.d.ts.map +1 -1
  85. package/dist/content/excerpt.js +2 -7
  86. package/dist/content/excerpt.js.map +1 -1
  87. package/dist/content/post-fields.d.ts +1 -1
  88. package/dist/content/post-fields.d.ts.map +1 -1
  89. package/dist/content/post-fields.js +30 -35
  90. package/dist/content/post-fields.js.map +1 -1
  91. package/dist/content/post-type.d.ts +2 -3
  92. package/dist/content/post-type.d.ts.map +1 -1
  93. package/dist/content/post-type.js +8 -9
  94. package/dist/content/post-type.js.map +1 -1
  95. package/dist/content/review-form.d.ts +49 -0
  96. package/dist/content/review-form.d.ts.map +1 -0
  97. package/dist/content/review-form.js +282 -0
  98. package/dist/content/review-form.js.map +1 -0
  99. package/dist/content/review.d.ts +57 -0
  100. package/dist/content/review.d.ts.map +1 -0
  101. package/dist/content/review.js +264 -0
  102. package/dist/content/review.js.map +1 -0
  103. package/dist/content/write.d.ts.map +1 -1
  104. package/dist/content/write.js +6 -9
  105. package/dist/content/write.js.map +1 -1
  106. package/dist/exports.d.ts +1 -1
  107. package/dist/exports.d.ts.map +1 -1
  108. package/dist/federation/article.d.ts.map +1 -1
  109. package/dist/federation/article.js +27 -29
  110. package/dist/federation/article.js.map +1 -1
  111. package/dist/federation/federation.d.ts +3 -0
  112. package/dist/federation/federation.d.ts.map +1 -1
  113. package/dist/federation/federation.js +5 -1
  114. package/dist/federation/federation.js.map +1 -1
  115. package/dist/federation/refusals.d.ts +18 -0
  116. package/dist/federation/refusals.d.ts.map +1 -0
  117. package/dist/federation/refusals.js +84 -0
  118. package/dist/federation/refusals.js.map +1 -0
  119. package/dist/images/variants.d.ts +1 -0
  120. package/dist/images/variants.d.ts.map +1 -1
  121. package/dist/images/variants.js +10 -0
  122. package/dist/images/variants.js.map +1 -1
  123. package/dist/item-lookup/item-id.d.ts +33 -0
  124. package/dist/item-lookup/item-id.d.ts.map +1 -0
  125. package/dist/item-lookup/item-id.js +71 -0
  126. package/dist/item-lookup/item-id.js.map +1 -0
  127. package/dist/item-lookup/lookup.d.ts +58 -0
  128. package/dist/item-lookup/lookup.d.ts.map +1 -0
  129. package/dist/item-lookup/lookup.js +532 -0
  130. package/dist/item-lookup/lookup.js.map +1 -0
  131. package/dist/micropub/create.d.ts +1 -1
  132. package/dist/micropub/create.d.ts.map +1 -1
  133. package/dist/micropub/create.js +4 -2
  134. package/dist/micropub/create.js.map +1 -1
  135. package/dist/micropub/endpoint.d.ts.map +1 -1
  136. package/dist/micropub/endpoint.js +3 -10
  137. package/dist/micropub/endpoint.js.map +1 -1
  138. package/dist/micropub/update.d.ts.map +1 -1
  139. package/dist/micropub/update.js +12 -6
  140. package/dist/micropub/update.js.map +1 -1
  141. package/dist/plugin.d.ts +26 -2
  142. package/dist/plugin.d.ts.map +1 -1
  143. package/dist/plugin.js.map +1 -1
  144. package/dist/settings/names.d.ts +1 -1
  145. package/dist/settings/names.d.ts.map +1 -1
  146. package/dist/settings/names.js +8 -1
  147. package/dist/settings/names.js.map +1 -1
  148. package/dist/settings/registry.d.ts.map +1 -1
  149. package/dist/settings/registry.js +2 -1
  150. package/dist/settings/registry.js.map +1 -1
  151. package/dist/settings/tmdb.d.ts +5 -0
  152. package/dist/settings/tmdb.d.ts.map +1 -0
  153. package/dist/settings/tmdb.js +22 -0
  154. package/dist/settings/tmdb.js.map +1 -0
  155. package/dist/store/profiles.d.ts +3 -2
  156. package/dist/store/profiles.d.ts.map +1 -1
  157. package/dist/store/profiles.js +2 -4
  158. package/dist/store/profiles.js.map +1 -1
  159. package/dist/web/context.d.ts +9 -21
  160. package/dist/web/context.d.ts.map +1 -1
  161. package/dist/web/context.js +20 -20
  162. package/dist/web/context.js.map +1 -1
  163. package/dist/web/feed-item.d.ts +6 -6
  164. package/dist/web/feed-item.d.ts.map +1 -1
  165. package/dist/web/feed-item.js +7 -3
  166. package/dist/web/feed-item.js.map +1 -1
  167. package/dist/web/plugin-federation.d.ts +2 -0
  168. package/dist/web/plugin-federation.d.ts.map +1 -1
  169. package/dist/web/plugin-federation.js +3 -0
  170. package/dist/web/plugin-federation.js.map +1 -1
  171. package/dist/webmention/cited-picture.d.ts +1 -0
  172. package/dist/webmention/cited-picture.d.ts.map +1 -1
  173. package/dist/webmention/cited-picture.js +7 -4
  174. package/dist/webmention/cited-picture.js.map +1 -1
  175. package/dist/webmention/fetch-public.d.ts +3 -0
  176. package/dist/webmention/fetch-public.d.ts.map +1 -1
  177. package/dist/webmention/fetch-public.js +1 -1
  178. package/dist/webmention/fetch-public.js.map +1 -1
  179. package/dist/webmention/reply-context/json-ld.d.ts +8 -0
  180. package/dist/webmention/reply-context/json-ld.d.ts.map +1 -1
  181. package/dist/webmention/reply-context/json-ld.js +21 -0
  182. package/dist/webmention/reply-context/json-ld.js.map +1 -1
  183. package/dist/webmention/service.d.ts.map +1 -1
  184. package/dist/webmention/service.js +3 -5
  185. package/dist/webmention/service.js.map +1 -1
  186. package/package.json +1 -1
  187. package/themes/default/layouts/post.njk +6 -5
  188. package/themes/default/partials/post-list.njk +15 -20
  189. package/themes/default/partials/review.njk +1 -0
  190. package/themes/default/src/style.css +24 -4
  191. package/themes/default/static/style.css +33 -5
  192. package/themes/default/partials/read.njk +0 -5
@@ -1,4 +1,4 @@
1
- /* The media screen's copy buttons, and nothing else.
1
+ /* The copy buttons beside readonly fields, and nothing else.
2
2
 
3
3
  Every URL and every ready-made Markdown line is already a readonly text
4
4
  input, which selects and copies like any other text, so the screen is
@@ -17,30 +17,37 @@
17
17
  button appears. */
18
18
  if (!navigator.clipboard) return;
19
19
 
20
- var buttons = document.querySelectorAll('button[data-copy]');
21
- if (!buttons.length) return;
22
-
23
- Array.prototype.forEach.call(buttons, function (button) {
24
- var field = document.getElementById(button.getAttribute('data-copy'));
25
- if (!field) return;
26
-
27
- button.hidden = false;
28
- button.addEventListener('click', function () {
29
- navigator.clipboard.writeText(field.value).then(
30
- function () {
31
- said(button, 'Copied');
32
- },
33
- function () {
34
- /* Refused, usually because the document was not focused. Selecting
35
- the text is then the useful thing to do: the keyboard shortcut
36
- everybody already knows takes it from there. */
37
- field.select();
38
- said(button, 'Press ⌘C');
39
- },
40
- );
41
- });
20
+ enhance(document);
21
+
22
+ /* media.js shows a details screen in a dialog after this has run, and
23
+ announces the content it inserted. */
24
+ document.addEventListener('geekity:inserted', function (event) {
25
+ enhance(event.target);
42
26
  });
43
27
 
28
+ function enhance(root) {
29
+ Array.prototype.forEach.call(root.querySelectorAll('button[data-copy]'), function (button) {
30
+ var field = document.getElementById(button.getAttribute('data-copy'));
31
+ if (!field) return;
32
+
33
+ button.hidden = false;
34
+ button.addEventListener('click', function () {
35
+ navigator.clipboard.writeText(field.value).then(
36
+ function () {
37
+ said(button, 'Copied');
38
+ },
39
+ function () {
40
+ /* Refused, usually because the document was not focused. Selecting
41
+ the text is then the useful thing to do: the keyboard shortcut
42
+ everybody already knows takes it from there. */
43
+ field.select();
44
+ said(button, 'Press ⌘C');
45
+ },
46
+ );
47
+ });
48
+ });
49
+ }
50
+
44
51
  /* Say something on the button for a moment, then put its label back. */
45
52
  function said(button, message) {
46
53
  if (button.dataset.label === undefined) button.dataset.label = button.textContent;
@@ -0,0 +1,179 @@
1
+ (function () {
2
+ var press = document.querySelector('button[data-item-lookup]');
3
+ var status = document.getElementById('editor-item-lookup-status');
4
+ if (!press || !status || !press.form) return;
5
+ var form = press.form;
6
+ var url = press.getAttribute('data-item-lookup');
7
+
8
+ function plainForm() {
9
+ var fields = new URLSearchParams();
10
+ new FormData(form).forEach(function (value, name) {
11
+ if (typeof value === 'string') fields.append(name, value);
12
+ });
13
+ return fields;
14
+ }
15
+
16
+ function tell(line, message, failed) {
17
+ line.textContent = message;
18
+ line.classList.toggle('text-error', failed === true);
19
+ line.classList.toggle('text-base-content', failed !== true);
20
+ }
21
+
22
+ function say(message, failed) {
23
+ tell(status, message, failed);
24
+ }
25
+
26
+ function answerOf(response) {
27
+ var type = response.headers.get('content-type') || '';
28
+ if (type.indexOf('application/json') === -1) {
29
+ throw new Error(
30
+ response.status === 403
31
+ ? 'The site refused the request because this page is out of date. Copy your changes somewhere safe, then reload the page.'
32
+ : response.redirected
33
+ ? 'You are signed out. Sign in again in another tab, then try again.'
34
+ : 'The site answered ' + response.status + ' instead of JSON.',
35
+ );
36
+ }
37
+ return response.json();
38
+ }
39
+
40
+ function lookUp(pick) {
41
+ var fields = plainForm();
42
+ if (pick) fields.set('item-pick', pick);
43
+ press.disabled = true;
44
+ say('Looking it up…');
45
+
46
+ return fetch(url, {
47
+ method: 'POST',
48
+ body: fields,
49
+ credentials: 'same-origin',
50
+ headers: { accept: 'application/json' },
51
+ })
52
+ .then(answerOf)
53
+ .then(function (answer) {
54
+ var filled = answer.fields || {};
55
+ Object.keys(filled).forEach(function (name) {
56
+ var box = form.elements.namedItem(name);
57
+ if (box && box.value === '') box.value = String(filled[name]);
58
+ });
59
+ var preview = document.getElementById('editor-item-photo-preview');
60
+ if (preview && answer.thumbnail && filled['item-photo']) {
61
+ preview.src = answer.thumbnail;
62
+ preview.hidden = false;
63
+ }
64
+ say(String(answer.message || ''), !answer.ok);
65
+ })
66
+ .catch(function (error) {
67
+ say(error.message || 'The lookup failed.', true);
68
+ })
69
+ .then(function () {
70
+ press.disabled = false;
71
+ });
72
+ }
73
+
74
+ press.addEventListener('click', function (event) {
75
+ event.preventDefault();
76
+ lookUp('');
77
+ });
78
+
79
+ var find = document.getElementById('editor-item-find');
80
+ var query = document.getElementById('editor-item-find-query');
81
+ var search = document.getElementById('editor-item-find-button');
82
+ var said = document.getElementById('editor-item-find-status');
83
+ var list = document.getElementById('editor-item-find-matches');
84
+ var credit = document.getElementById('editor-item-find-credit');
85
+ var shape = document.getElementById('editor-item-find-match');
86
+ if (!find || !query || !search || !said || !list || !credit || !shape) return;
87
+ find.hidden = false;
88
+ var searching = false;
89
+
90
+ function chosenKind() {
91
+ var ticked = form.querySelector('input[name="item-kind"]:checked');
92
+ return ticked ? ticked.value : '';
93
+ }
94
+
95
+ function clear() {
96
+ Array.prototype.slice.call(list.querySelectorAll('li[data-match]')).forEach(function (row) {
97
+ row.remove();
98
+ });
99
+ credit.hidden = true;
100
+ list.hidden = true;
101
+ }
102
+
103
+ function show(matches) {
104
+ clear();
105
+ matches.forEach(function (match) {
106
+ var row = shape.content.firstElementChild.cloneNode(true);
107
+ row.setAttribute('data-match', '');
108
+ var picture = row.querySelector('img');
109
+ if (match.thumbnail) {
110
+ picture.src = match.thumbnail;
111
+ picture.hidden = false;
112
+ row.querySelector('[data-no-picture]').hidden = true;
113
+ }
114
+ row.querySelector('[data-name]').textContent = match.name;
115
+ row.querySelector('[data-detail]').textContent = match.detail;
116
+ row.querySelector('[data-source]').textContent = match.source;
117
+ row.querySelector('button').addEventListener('click', function () {
118
+ clear();
119
+ tell(said, '');
120
+ lookUp(match.id);
121
+ });
122
+ list.insertBefore(row, credit);
123
+ });
124
+ }
125
+
126
+ function run() {
127
+ if (searching) return;
128
+ searching = true;
129
+ search.disabled = true;
130
+ tell(said, 'Searching…');
131
+ var address =
132
+ find.getAttribute('data-item-search') +
133
+ '?' +
134
+ new URLSearchParams({ kind: chosenKind(), q: query.value }).toString();
135
+
136
+ fetch(address, { credentials: 'same-origin', headers: { accept: 'application/json' } })
137
+ .then(answerOf)
138
+ .then(function (answer) {
139
+ var matches = answer.matches || [];
140
+ show(matches);
141
+ if (answer.credit) {
142
+ credit.textContent = answer.credit;
143
+ credit.hidden = false;
144
+ }
145
+ list.hidden = !answer.ok;
146
+ tell(
147
+ said,
148
+ answer.ok
149
+ ? matches.length === 0
150
+ ? 'Nothing matches “' + query.value.trim() + '”.'
151
+ : matches.length === 1
152
+ ? 'One match. Pick it to fill in the item.'
153
+ : matches.length + ' matches. Pick one to fill in the item.'
154
+ : String(answer.message || ''),
155
+ !answer.ok,
156
+ );
157
+ })
158
+ .catch(function (error) {
159
+ clear();
160
+ tell(said, error.message || 'The search failed.', true);
161
+ })
162
+ .then(function () {
163
+ searching = false;
164
+ search.disabled = false;
165
+ });
166
+ }
167
+
168
+ search.addEventListener('click', run);
169
+ query.addEventListener('keydown', function (event) {
170
+ if (event.key !== 'Enter') return;
171
+ event.preventDefault();
172
+ run();
173
+ });
174
+ form.addEventListener('change', function (event) {
175
+ if (event.target.name !== 'item-kind') return;
176
+ clear();
177
+ tell(said, '');
178
+ });
179
+ })();
@@ -0,0 +1,82 @@
1
+ /* The media library's details dialog (TASK-429).
2
+
3
+ Every tile is already a link to its file's details screen, so the library
4
+ is complete before this file loads. This opens that screen in a dialog
5
+ instead: it fetches the page the link names, takes its
6
+ [data-media-details], and shows it over the grid. Previous and Next inside
7
+ the dialog, and the left and right arrow keys, fetch the neighbouring file
8
+ the same way. Anything that goes wrong falls back to following the link.
9
+
10
+ Forms inside the dialog post as they would on the details screen, which is
11
+ where their answers land.
12
+
13
+ Plain ES5-shaped browser JavaScript, like copy.js: it is served as it is
14
+ written. */
15
+ (function () {
16
+ var tiles = document.querySelectorAll('a[data-media-tile]');
17
+ if (!tiles.length || !window.fetch) return;
18
+
19
+ var dialog = document.createElement('dialog');
20
+ dialog.className = 'modal';
21
+ dialog.setAttribute('aria-label', 'File details');
22
+ dialog.innerHTML =
23
+ '<div class="modal-box max-w-6xl">' +
24
+ '<form method="dialog" class="mb-2 text-right"><button class="btn btn-sm btn-ghost">Close</button></form>' +
25
+ '<div data-media-dialog-body></div>' +
26
+ '</div>' +
27
+ '<form method="dialog" class="modal-backdrop"><button>Close</button></form>';
28
+ document.body.appendChild(dialog);
29
+ dialog.tabIndex = -1;
30
+ var body = dialog.querySelector('[data-media-dialog-body]');
31
+
32
+ Array.prototype.forEach.call(tiles, function (tile) {
33
+ tile.addEventListener('click', function (event) {
34
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
35
+ return;
36
+ }
37
+ event.preventDefault();
38
+ show(tile.href);
39
+ });
40
+ });
41
+
42
+ body.addEventListener('click', function (event) {
43
+ var link = event.target.closest('a[data-media-step], a[data-media-library]');
44
+ if (!link) return;
45
+ event.preventDefault();
46
+ if (link.hasAttribute('data-media-library')) dialog.close();
47
+ else show(link.href);
48
+ });
49
+
50
+ dialog.addEventListener('keydown', function (event) {
51
+ if (event.target.closest('input, textarea, select')) return;
52
+ var rel = event.key === 'ArrowLeft' ? 'prev' : event.key === 'ArrowRight' ? 'next' : '';
53
+ if (!rel) return;
54
+ var link = body.querySelector('a[rel="' + rel + '"]');
55
+ if (!link) return;
56
+ event.preventDefault();
57
+ show(link.href);
58
+ });
59
+
60
+ function show(url) {
61
+ fetch(url, { credentials: 'same-origin' })
62
+ .then(function (response) {
63
+ if (!response.ok) throw new Error(String(response.status));
64
+ return response.text();
65
+ })
66
+ .then(function (html) {
67
+ /* A template's content is inert: the other page's nonced <style>
68
+ elements are neither applied nor checked against this page's CSP. */
69
+ var parsed = document.createElement('template');
70
+ parsed.innerHTML = html;
71
+ var details = parsed.content.querySelector('[data-media-details]');
72
+ if (!details) throw new Error('no details');
73
+ body.replaceChildren(document.importNode(details, true));
74
+ body.dispatchEvent(new CustomEvent('geekity:inserted', { bubbles: true }));
75
+ if (!dialog.open) dialog.showModal();
76
+ dialog.focus();
77
+ })
78
+ .catch(function () {
79
+ window.location.href = url;
80
+ });
81
+ }
82
+ })();
@@ -1 +1 @@
1
- {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/admin/documents.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/admin/documents.ts"],"names":[],"mappings":"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"}
@@ -5,13 +5,13 @@ import { PAGE_KIND, POST_KIND } from '../content/document-kind.js';
5
5
  import { TRANSCRIPT_TYPES } from '../content/enclosure.js';
6
6
  import { ENCLOSURE_FIELDS } from '../content/enclosure-form.js';
7
7
  import { EVENT_FIELDS } from '../content/event-form.js';
8
+ import { isReviewKind, REVIEW_KIND_FIELDS, REVIEW_KIND_WORDS, REVIEW_KINDS, } from '../content/review.js';
9
+ import { REVIEW_FIELDS } from '../content/review-form.js';
8
10
  import { BLANK_LOCATION_FORM } from '../content/location-form.js';
9
11
  import { postLocations } from '../content/locations.js';
10
12
  import { PHOTO_FIELDS } from '../content/photo-form.js';
11
13
  import { syndicateToField } from '../content/post-fields.js';
12
14
  import { postLabel } from '../content/post-type.js';
13
- import { isReadStatus, READ_STATUS_LABELS, READ_STATUSES } from '../content/read.js';
14
- import { READ_FIELDS } from '../content/read-form.js';
15
15
  import { RSVP_LABELS, RSVP_VALUES, rsvpValue } from '../content/rsvp.js';
16
16
  import { scheduledFor } from '../content/schedule.js';
17
17
  import { isTrashedPath } from '../content/store.js';
@@ -21,16 +21,18 @@ import { isVisibility, visibilityOf } from '../content/visibility.js';
21
21
  import { missingAltText, writeDocument } from '../content/write.js';
22
22
  import { currentUsername, documentSite } from '../web/document-site.js';
23
23
  import { normalizeBody, serializeDocument } from '../content/writer.js';
24
- import { readAltTexts } from '../content/alt-text.js';
25
24
  import { listUsers } from '../site/users.js';
26
25
  import { stringOrEmpty } from '../util/forms.js';
27
26
  import { userForAuthor } from '../content/authors.js';
28
27
  import { isServed } from '../content/served.js';
28
+ import { SEARCH_KINDS } from '../item-lookup/lookup.js';
29
29
  import { syndicationTargetsReader } from '../webmention/syndication.js';
30
30
  import { editorActionViews } from './editor-actions.js';
31
- import { BLANK_ALTERNATE_ROW, enclosureChoices, LOCATION_FIELDS, photoChoices, photoRowViews, readLocationForm, } from './editor-fields.js';
32
- import { openGroups } from './editor-layout.js';
31
+ import { BLANK_ALTERNATE_ROW, enclosureChoices, LOCATION_FIELDS, libraryThumbnail, photoPicker, readLocationForm, } from './editor-fields.js';
32
+ import { discoveredKind, EDITOR_KINDS, isPostKind, PAGE_CHOICE, POST_KINDS, REVIEW_EDITOR_WORDS, } from './editor-kinds.js';
33
+ import { editorSections, openGroups, sidebarLocation } from './editor-layout.js';
33
34
  import { flash } from './flash.js';
35
+ import { ITEM_LOOKUP_BUTTON, ITEM_LOOKUP_PATH, ITEM_SEARCH_PATH, lookUpReviewItem, } from './item-lookup.js';
34
36
  import { formatInTimezone } from './formatting.js';
35
37
  import { allowGeolocation } from './headers.js';
36
38
  import { PREVIEW_PATH } from './preview.js';
@@ -146,13 +148,24 @@ export function mountDocumentScreens(app, options) {
146
148
  });
147
149
  // Registered before the slug route, so a document can never take the URL of
148
150
  // the form that makes a new one.
149
- app.get(newEditorPath(kind), (c) => renderEditor(c, {
150
- kind,
151
- render,
152
- document: undefined,
153
- form: blankForm(kind, siteTimezone(c), c.var.store.now()),
154
- }));
155
- app.post(newEditorPath(kind), async (c) => saveFromForm(c, { kind, render, document: undefined, body: await c.req.parseBody() }));
151
+ app.get(newEditorPath(kind), (c) => {
152
+ const postKind = c.req.query('kind');
153
+ if (kind.type === 'post' && !isPostKind(postKind))
154
+ return renderChooser(c, kind, render);
155
+ return renderEditor(c, {
156
+ kind,
157
+ render,
158
+ document: undefined,
159
+ form: blankForm(kind, siteTimezone(c), c.var.store.now()),
160
+ postKind: isPostKind(postKind) ? postKind : undefined,
161
+ });
162
+ });
163
+ app.post(newEditorPath(kind), async (c) => {
164
+ const body = await c.req.parseBody();
165
+ return (switchKind(c, { kind, render, document: undefined, body }) ??
166
+ lookUpItem(c, { kind, render, document: undefined, body }) ??
167
+ saveFromForm(c, { kind, render, document: undefined, body }));
168
+ });
156
169
  app.get(`${kind.basePath}/:slug`, (c) => {
157
170
  const document = findEdited(c.var.store, kind, c.req.param('slug'), c.req.query('path'));
158
171
  if (document === undefined)
@@ -174,7 +187,9 @@ export function mountDocumentScreens(app, options) {
174
187
  if (action === 'trash' || action === 'restore') {
175
188
  return moveDocument(c, { kind, document, action, returnTo: stringOrEmpty(body['return']) });
176
189
  }
177
- return saveFromForm(c, { kind, render, document, body });
190
+ return (switchKind(c, { kind, render, document, body }) ??
191
+ lookUpItem(c, { kind, render, document, body }) ??
192
+ saveFromForm(c, { kind, render, document, body }));
178
193
  });
179
194
  }
180
195
  /**
@@ -183,7 +198,7 @@ export function mountDocumentScreens(app, options) {
183
198
  */
184
199
  export function submittedForm(kind, body, contentDir) {
185
200
  const declared = syndicationTargetsReader(contentDir)();
186
- return {
201
+ const form = {
187
202
  title: stringOrEmpty(body['title']).trim(),
188
203
  slug: stringOrEmpty(body['slug']).trim(),
189
204
  permalink: stringOrEmpty(body['permalink']).trim(),
@@ -204,6 +219,61 @@ export function submittedForm(kind, body, contentDir) {
204
219
  body: normalizeBody(stringOrEmpty(body['body'])),
205
220
  hash: stringOrEmpty(body['hash']),
206
221
  };
222
+ return kind.replies ? withLink(form, body) : form;
223
+ }
224
+ const LINK_FIELDS = { 'in-reply-to': 'inReplyTo', ...CITATION_FIELDS };
225
+ function withLink(form, body) {
226
+ const verb = stringOrEmpty(body['link-verb']);
227
+ if (!Object.hasOwn(LINK_FIELDS, verb) || Object.hasOwn(body, verb))
228
+ return form;
229
+ return {
230
+ ...form,
231
+ [LINK_FIELDS[verb]]: stringOrEmpty(body['link-url']).trim(),
232
+ };
233
+ }
234
+ function switchKind(c, options) {
235
+ const { kind, render, document, body } = options;
236
+ const postKind = body['switch-kind'];
237
+ if (kind.type !== 'post' || !isPostKind(postKind))
238
+ return undefined;
239
+ return renderEditor(c, {
240
+ kind,
241
+ render,
242
+ document,
243
+ form: submittedForm(kind, body, c.var.config.contentDir),
244
+ postKind,
245
+ });
246
+ }
247
+ function lookUpItem(c, options) {
248
+ const { kind, render, document, body } = options;
249
+ if (kind.type !== 'post' || body[ITEM_LOOKUP_BUTTON] === undefined)
250
+ return undefined;
251
+ return (async () => {
252
+ const form = submittedForm(kind, body, c.var.config.contentDir);
253
+ const outcome = await lookUpReviewItem(c, form.review);
254
+ return renderEditor(c, {
255
+ kind,
256
+ render,
257
+ document,
258
+ form: { ...form, review: outcome.review },
259
+ postKind: 'review',
260
+ itemLookup: { message: outcome.message, failed: outcome.failed },
261
+ });
262
+ })();
263
+ }
264
+ function renderChooser(c, kind, render) {
265
+ return render(c, ADMIN_TEMPLATES.documentChooser, {
266
+ section: kind.section,
267
+ child: 'new',
268
+ kinds: [
269
+ ...POST_KINDS.map((key) => ({
270
+ ...EDITOR_KINDS[key],
271
+ url: `${newEditorPath(kind)}?kind=${key}`,
272
+ page: false,
273
+ })),
274
+ { ...PAGE_CHOICE, url: newEditorPath(PAGE_SCREENS), page: true },
275
+ ],
276
+ });
207
277
  }
208
278
  /**
209
279
  * Write what the editor submitted through {@link writeDocument}, and answer
@@ -225,6 +295,7 @@ async function saveFromForm(c, options) {
225
295
  render,
226
296
  document,
227
297
  form: { ...form, draft },
298
+ postKind: isPostKind(body['kind']) ? body['kind'] : undefined,
228
299
  refusal,
229
300
  status: 400,
230
301
  });
@@ -408,6 +479,9 @@ function siteDefaultLabel(type, policy) {
408
479
  */
409
480
  async function renderEditor(c, options) {
410
481
  const { kind, document, form } = options;
482
+ const postKind = options.postKind ?? discoveredKind(form);
483
+ const editorKind = kind.type === 'post' ? EDITOR_KINDS[postKind] : undefined;
484
+ const sections = editorSections(postKind, form);
411
485
  const trashed = document !== undefined && isTrashedPath(document.path);
412
486
  const now = c.var.store.now();
413
487
  const timezone = siteTimezone(c);
@@ -449,21 +523,45 @@ async function renderEditor(c, options) {
449
523
  enclosureChoices: await enclosureChoices(c.var.config.contentDir, form.enclosure.url),
450
524
  transcriptTypes: TRANSCRIPT_TYPES,
451
525
  alternateRows: [...form.enclosure.alternates, BLANK_ALTERNATE_ROW],
526
+ postKind,
527
+ editorKind,
528
+ postKinds: POST_KINDS.map((key) => ({ key, ...EDITOR_KINDS[key] })),
529
+ sections,
530
+ sidebarLocation: sidebarLocation(postKind, form),
531
+ link: linkView(form, sections.shown.rsvp),
452
532
  photoFields: PHOTO_FIELDS,
453
- photoRows: photoRowViews(form.photos, readAltTexts(c.var.config.contentDir)),
454
- photoChoices: await photoChoices(c.var.config.contentDir),
533
+ photos: await photoPicker(c.var.config, form.photos),
534
+ reviewPhotoThumbnail: libraryThumbnail(c.var.config, form.review.photo),
455
535
  locationFields: LOCATION_FIELDS,
456
536
  ...citedPreviews(c, form),
457
537
  rsvpChoices: RSVP_VALUES.map((value) => ({ value, label: RSVP_LABELS[value] })),
458
538
  ...(form.rsvp === '' || rsvpValue(form.rsvp) !== undefined
459
539
  ? {}
460
540
  : { unrecognizedRsvp: form.rsvp }),
461
- readFields: READ_FIELDS,
462
541
  eventFields: EVENT_FIELDS,
463
- readStatuses: READ_STATUSES.map((value) => ({ value, label: READ_STATUS_LABELS[value] })),
464
- ...(form.readStatus === '' || isReadStatus(form.readStatus)
542
+ reviewFields: REVIEW_FIELDS,
543
+ itemLookupButton: ITEM_LOOKUP_BUTTON,
544
+ itemLookupUrl: ITEM_LOOKUP_PATH,
545
+ itemLookup: options.itemLookup,
546
+ itemSearchUrl: ITEM_SEARCH_PATH,
547
+ itemSearchKinds: SEARCH_KINDS,
548
+ reviewKinds: REVIEW_KINDS.map((value) => ({
549
+ value,
550
+ ...REVIEW_KIND_WORDS[value],
551
+ ...REVIEW_EDITOR_WORDS[value],
552
+ })),
553
+ reviewKindsWith: Object.fromEntries(['by', 'year', 'uid', 'url', 'photo', 'date', 'season', 'episode', 'category'].map((field) => [
554
+ field,
555
+ REVIEW_KINDS.filter((value) => REVIEW_KIND_FIELDS[value].includes(field)),
556
+ ])),
557
+ reviewKindChosen: form.review.kind === ''
558
+ ? sections.filled.review
559
+ ? 'other'
560
+ : 'book'
561
+ : form.review.kind,
562
+ ...(form.review.kind === '' || isReviewKind(form.review.kind)
465
563
  ? {}
466
- : { unrecognizedReadStatus: form.readStatus }),
564
+ : { unrecognizedReviewKind: form.review.kind }),
467
565
  // TASK-155: one checkbox per target the site declares.
468
566
  syndicationTargets: syndicationTargetsReader(c.var.config.contentDir)().map((target) => ({
469
567
  ...target,
@@ -478,8 +576,9 @@ async function renderEditor(c, options) {
478
576
  // What an empty Language field means.
479
577
  siteLanguage: c.var.siteAtRequestStart.language,
480
578
  heading: document === undefined
481
- ? `Add ${kind.singular}`
482
- : `Edit ${kind.singular}: ${postLabel(document, (url) => c.var.replyContexts.read(url, c.var.siteAtRequestStart))}`,
579
+ ? `New ${editorKind?.noun ?? kind.singular}`
580
+ : `Edit ${editorKind?.noun ?? kind.singular}: ${postLabel(document, (url) => c.var.replyContexts.read(url, c.var.siteAtRequestStart))}`,
581
+ chooserUrl: newEditorPath(kind),
483
582
  saveUrl: document === undefined ? newEditorPath(kind) : documentEditorPath(kind, document),
484
583
  listUrl: kind.basePath,
485
584
  previewUrl: PREVIEW_PATH,
@@ -495,6 +594,26 @@ async function renderEditor(c, options) {
495
594
  open: openGroups(form, options.refusal?.field),
496
595
  });
497
596
  }
597
+ const LINK_VERBS = [
598
+ { property: 'in-reply-to', label: 'Replies to', also: 'Also replies to' },
599
+ { property: 'like-of', label: 'Likes', also: 'Also likes' },
600
+ { property: 'repost-of', label: 'Reposts', also: 'Also reposts' },
601
+ { property: 'bookmark-of', label: 'Bookmarks', also: 'Also bookmarks' },
602
+ ];
603
+ const LINK_ORDER = ['repost-of', 'like-of', 'in-reply-to', 'bookmark-of'];
604
+ function linkView(form, rsvpShown) {
605
+ const value = (property) => form[LINK_FIELDS[property]];
606
+ const offered = LINK_VERBS.filter((verb) => !rsvpShown || verb.property !== 'in-reply-to');
607
+ const chosen = LINK_ORDER.find((property) => value(property) !== '' && offered.some((verb) => verb.property === property)) ?? offered[0]?.property;
608
+ return {
609
+ verbs: offered,
610
+ chosen,
611
+ url: chosen === undefined ? '' : value(chosen),
612
+ others: offered
613
+ .filter((verb) => verb.property !== chosen && value(verb.property) !== '')
614
+ .map((verb) => ({ ...verb, url: value(verb.property) })),
615
+ };
616
+ }
498
617
  function citedPreviews(c, form) {
499
618
  const cited = [
500
619
  ['in-reply-to', form.inReplyTo],