@kids-reporter/cms-core 0.1.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.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # [@mirrormedia/lilith-core](https://www.npmjs.com/package/@mirrormedia/lilith-core) · ![npm version](https://img.shields.io/npm/v/@mirrormedia/lilith-core.svg?style=flat)
2
+
3
+ ### Installation
4
+
5
+ `yarn install`
6
+
7
+ ### Development
8
+
9
+ `@mirrormedia/lilith-core` 被 `@mirrormedia/lilith-(vision|mesh|editools)` 所依賴,因此,在開發 lilith-core 的程式碼時,
10
+ 可以透過 lilith-(vision|mesh|editools) 來協助開發。
11
+
12
+ 舉 lilith-editools 為例,
13
+ 我們可以先起 lilith-editools server,接著修改 lilith-core 的程式碼,
14
+ 修改到一個階段,想要測試,便在 `packages/core` 底下跑 `yarn build`,產生新的 transpiled 程式碼。
15
+ 因為 lilith-editools 和 lilith-core 都在 monorepo 中,yarn workspaces 會為 lilith-core pkg 建立 soft link,
16
+ 將 `node_modules/@mirrormedia/lilith-core` 指到 `packages/core`,所以 `yarn build` 產生的新的程式碼,
17
+ 可以不需要透過 npm publish 和 yarn install 的方式,立即讓 lilith-editools 使用。
18
+ 等到確定程式碼修改完畢後,我們再將最新的程式碼上傳(`npm publish`)到 npm registry 去,讓 lilith-editools 的 CI/CD 可以下載到最新的版本。
19
+
20
+ #### After v1.2.6
21
+
22
+ 使用 `@mirrormedia/lilith-core` 的 richTextEditor 時,需要帶入 `website` 的參數以便 `@mirrormedia/lilith-core` 引用的對應網站的 draft-editor 版本,
23
+ 在新增 keystone 的情況下,可以暫時填入既有的 `website` 值先快速建立,日後若有需求可以再另外新增 website 在 `@mirrormedia/lilith-core`, `@mirrormedia/lilith-draft-editor` 和 `@mirrormedia/lilith-renderer` 中。
24
+
25
+ ### Build
26
+
27
+ `yarn build`
28
+
29
+ ### Publish
30
+
31
+ `npm run publish`
32
+
33
+ 在 publish 前,請根據 conventional commits 的規範,將 package.json#version 升版。
34
+
35
+ ### Notable Details
36
+
37
+ #### For those files under `views/` folder, we transpile them specifically.
38
+
39
+ For those files under `views/` folder, we transpile them by babel according to different configuation.
40
+ The specific babel configuration is `.views.babelrc.js`.
41
+ In `.views.babelrc.js`, we tell babel not to transpile `import` and `export` es6 codes into commonJS codes.
42
+ The Keystone server won't start server well if those files under `views/` are transpiled into commonJS codes.
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.richTextEditor = void 0;
7
+
8
+ var _types = require("@keystone-6/core/types");
9
+
10
+ var _core = require("@keystone-6/core");
11
+
12
+ const richTextEditor = ({
13
+ defaultValue = null,
14
+ disabledButtons = [],
15
+ ...config
16
+ } = {}) => meta => {
17
+ var _config$db;
18
+
19
+ if (config.isIndexed === 'unique') {
20
+ throw Error("isIndexed: 'unique' is not a supported option for field type textEditor");
21
+ }
22
+
23
+ const resolve = val => val === null && meta.provider === 'postgresql' ? 'DbNull' : val;
24
+
25
+ return (0, _types.jsonFieldTypePolyfilledForSQLite)(meta.provider, { ...config,
26
+ input: {
27
+ create: {
28
+ arg: _core.graphql.arg({
29
+ type: _core.graphql.JSON
30
+ }),
31
+
32
+ resolve(val) {
33
+ return resolve(val === undefined ? defaultValue : val);
34
+ }
35
+
36
+ },
37
+ update: {
38
+ arg: _core.graphql.arg({
39
+ type: _core.graphql.JSON
40
+ }),
41
+ resolve
42
+ }
43
+ },
44
+ output: _core.graphql.field({
45
+ type: _core.graphql.JSON
46
+ }),
47
+ views: `@kids-reporter/cms-core/lib/custom-fields/rich-text-editor/views/index`,
48
+ getAdminMeta: () => ({
49
+ defaultValue,
50
+ disabledButtons
51
+ })
52
+ }, {
53
+ default: defaultValue === null ? undefined : {
54
+ kind: 'literal',
55
+ value: JSON.stringify(defaultValue)
56
+ },
57
+ map: (_config$db = config.db) === null || _config$db === void 0 ? void 0 : _config$db.map
58
+ });
59
+ };
60
+
61
+ exports.richTextEditor = richTextEditor;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.controller = exports.Field = exports.Cell = exports.CardValue = void 0;
7
+
8
+ var _react = _interopRequireDefault(require("react"));
9
+
10
+ var _core = require("@keystone-ui/core");
11
+
12
+ var _fields = require("@keystone-ui/fields");
13
+
14
+ var _components = require("@keystone-6/core/admin-ui/components");
15
+
16
+ var _draftJs = require("draft-js");
17
+
18
+ var _draftEditor = _interopRequireDefault(require("@kids-reporter/draft-editor"));
19
+
20
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
+
22
+ // eslint-disable-line
23
+ // import { RichTextEditor, decorators } from '@mirrormedia/lilith-draft-editor'
24
+ const {
25
+ RichTextEditor,
26
+ decorators
27
+ } = _draftEditor.default.DraftEditor;
28
+
29
+ const Field = ({
30
+ field,
31
+ value,
32
+ onChange,
33
+ autoFocus // eslint-disable-line
34
+
35
+ }) => {
36
+ return /*#__PURE__*/_react.default.createElement(_fields.FieldContainer, null, /*#__PURE__*/_react.default.createElement(_fields.FieldLabel, null, field.label, /*#__PURE__*/_react.default.createElement(_core.Stack, null, /*#__PURE__*/_react.default.createElement(RichTextEditor, {
37
+ disabledButtons: field.disabledButtons,
38
+ editorState: value,
39
+ onChange: editorState => onChange === null || onChange === void 0 ? void 0 : onChange(editorState)
40
+ }))));
41
+ };
42
+
43
+ exports.Field = Field;
44
+
45
+ const Cell = ({
46
+ item,
47
+ field,
48
+ linkTo
49
+ }) => {
50
+ const value = item[field.path] + '';
51
+ return linkTo ? /*#__PURE__*/_react.default.createElement(_components.CellLink, linkTo, value) : /*#__PURE__*/_react.default.createElement(_components.CellContainer, null, value);
52
+ };
53
+
54
+ exports.Cell = Cell;
55
+ Cell.supportsLinkTo = true;
56
+
57
+ const CardValue = ({
58
+ item,
59
+ field
60
+ }) => {
61
+ return /*#__PURE__*/_react.default.createElement(_fields.FieldContainer, null, /*#__PURE__*/_react.default.createElement(_fields.FieldLabel, null, field.label), item[field.path]);
62
+ };
63
+
64
+ exports.CardValue = CardValue;
65
+
66
+ const controller = config => {
67
+ var _config$fieldMeta;
68
+
69
+ return {
70
+ disabledButtons: ((_config$fieldMeta = config.fieldMeta) === null || _config$fieldMeta === void 0 ? void 0 : _config$fieldMeta.disabledButtons) ?? [],
71
+ path: config.path,
72
+ label: config.label,
73
+ graphqlSelection: config.path,
74
+ defaultValue: _draftJs.EditorState.createEmpty(decorators),
75
+ deserialize: data => {
76
+ const rawContentState = data[config.path];
77
+
78
+ if (rawContentState === null) {
79
+ return _draftJs.EditorState.createEmpty(decorators);
80
+ }
81
+
82
+ try {
83
+ const contentState = (0, _draftJs.convertFromRaw)(rawContentState);
84
+
85
+ const editorState = _draftJs.EditorState.createWithContent(contentState, decorators);
86
+
87
+ return editorState;
88
+ } catch (err) {
89
+ console.error(err);
90
+ return _draftJs.EditorState.createEmpty(decorators);
91
+ }
92
+ },
93
+ serialize: editorState => {
94
+ if (!editorState) {
95
+ return {
96
+ [config.path]: null
97
+ };
98
+ }
99
+
100
+ try {
101
+ const rawContentState = (0, _draftJs.convertToRaw)(editorState.getCurrentContent());
102
+ return {
103
+ [config.path]: rawContentState
104
+ };
105
+ } catch (err) {
106
+ console.error(err);
107
+ return {
108
+ [config.path]: null
109
+ };
110
+ }
111
+ }
112
+ };
113
+ };
114
+
115
+ exports.controller = controller;
package/lib/index.js ADDED
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.utils = exports.default = exports.customFields = void 0;
7
+
8
+ var _manualOrderRelationship = _interopRequireDefault(require("./utils/manual-order-relationship"));
9
+
10
+ var _draftConverter = _interopRequireDefault(require("@kids-reporter/draft-editor/lib/draft-converter"));
11
+
12
+ var _richTextEditor = require("./custom-fields/rich-text-editor");
13
+
14
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
+
16
+ const customFields = {
17
+ draftConverter: _draftConverter.default,
18
+ richTextEditor: _richTextEditor.richTextEditor
19
+ };
20
+ exports.customFields = customFields;
21
+ const utils = {
22
+ addManualOrderRelationshipFields: _manualOrderRelationship.default
23
+ };
24
+ exports.utils = utils;
25
+ var _default = {
26
+ customFields,
27
+ utils
28
+ };
29
+ exports.default = _default;
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+
8
+ var _core = require("@keystone-6/core");
9
+
10
+ var _fields = require("@keystone-6/core/fields");
11
+
12
+ /**
13
+ * For `relationship` field, KeystoneJS won't take user input order into account.
14
+ * Therefore, after the create/update operation is done,
15
+ * the order of relationship items maybe not be the same order as the user input order.
16
+ *
17
+ * This function
18
+ * - adds monitoring fields in the list
19
+ * - adds virtual fields in the list. These virtual fields could return relationship items in order.
20
+ * - decorate `list.hooks.resolveInput` to record the user input order in the monitoring fields
21
+ *
22
+ * For example, if we have two lists like
23
+ * ```
24
+ * const User = {
25
+ * fields: {
26
+ * name: text(),
27
+ * }
28
+ * }
29
+ *
30
+ * const Post = {
31
+ * fields: {
32
+ * title: text(),
33
+ * content: text(),
34
+ * authors: relationship({ref: 'User', many: true})
35
+ * }
36
+ * }
37
+ * ```
38
+ *
39
+ * if we want to snapshot the authors input order, we can use this function like
40
+ * ```
41
+ * const postList = addManualOrderRelationshipFields([
42
+ * {
43
+ * fieldName: 'manualOrderOfAuthors',
44
+ * fieldLabel: 'authors 手動排序結果',
45
+ * targetFieldName: 'authors', // the target field to record the user input order
46
+ * targetListName: 'User', // relationship list name
47
+ * targetListLabelField: 'name', // refer to `User.fields.name`
48
+ * }
49
+ * ])(Post)
50
+ * ```
51
+ *
52
+ * `addManualOrderRelationshipFields` will create another JSON field `manualOrderOfAuthors` and virtual field `authorsInInputOrder` in the list,
53
+ * and decorate `list.hooks.resolveInput` to record the update/create operation,
54
+ * if the operation modifies the order of the relationship field.
55
+ *
56
+ * `authorsInInputOrder` is a virtual field, which means its value is computed on-the-fly, not stored in the database. This virtual field combines relationship field `authors` and monitoring field `manualOrderOfAuthors` to sort the authors in specific input order.
57
+ */
58
+ function addManualOrderRelationshipFields(manualOrderFields = [], list) {
59
+ var _list$hooks;
60
+
61
+ manualOrderFields.forEach(mo => {
62
+ var _list$fields;
63
+
64
+ if (!((_list$fields = list.fields) !== null && _list$fields !== void 0 && _list$fields[mo.fieldName])) {
65
+ list.fields[mo.fieldName] = (0, _fields.json)({
66
+ label: mo.fieldLabel,
67
+ ui: {
68
+ itemView: {
69
+ fieldMode: 'read'
70
+ }
71
+ }
72
+ });
73
+ } // add virtual field definition
74
+
75
+
76
+ addVirtualFieldToReturnItemsInInputOrder(list, mo);
77
+ }); // decorate `resolveInput` hook
78
+
79
+ list.hooks = list.hooks || {};
80
+ const originResolveInput = (_list$hooks = list.hooks) === null || _list$hooks === void 0 ? void 0 : _list$hooks.resolveInput;
81
+
82
+ list.hooks.resolveInput = async props => {
83
+ let resolvedData = props.resolvedData;
84
+
85
+ if (typeof originResolveInput === 'function') {
86
+ resolvedData = await originResolveInput(props);
87
+ }
88
+
89
+ const {
90
+ item,
91
+ context
92
+ } = props; // check if create/update item has the fields
93
+ // we want to monitor
94
+
95
+ for (let i = 0; i < manualOrderFields.length; i++) {
96
+ var _resolvedData;
97
+
98
+ const {
99
+ targetFieldName,
100
+ fieldName,
101
+ targetListName,
102
+ targetListLabelField
103
+ } = manualOrderFields[i]; // if create/update operation creates/modifies the `${targetFieldName}` field
104
+
105
+ if ((_resolvedData = resolvedData) !== null && _resolvedData !== void 0 && _resolvedData[targetFieldName]) {
106
+ var _resolvedData$targetF3, _resolvedData$targetF4;
107
+
108
+ let currentOrder = []; // update operation due to `item` not being `undefiend`
109
+
110
+ if (item) {
111
+ var _resolvedData$targetF, _resolvedData$targetF2;
112
+
113
+ const previousOrder = Array.isArray(item[fieldName]) ? item[fieldName] : []; // user disconnects/removes some relationship items.
114
+
115
+ const disconnectIds = ((_resolvedData$targetF = resolvedData[targetFieldName]) === null || _resolvedData$targetF === void 0 ? void 0 : (_resolvedData$targetF2 = _resolvedData$targetF.disconnect) === null || _resolvedData$targetF2 === void 0 ? void 0 : _resolvedData$targetF2.map(obj => obj.id.toString())) || []; // filtered out to-be-disconnected relationship items
116
+
117
+ currentOrder = previousOrder.filter(({
118
+ id
119
+ }) => {
120
+ return disconnectIds.indexOf(id) === -1;
121
+ });
122
+ } // user connects/adds some relationship item.
123
+
124
+
125
+ const connectedIds = ((_resolvedData$targetF3 = resolvedData[targetFieldName]) === null || _resolvedData$targetF3 === void 0 ? void 0 : (_resolvedData$targetF4 = _resolvedData$targetF3.connect) === null || _resolvedData$targetF4 === void 0 ? void 0 : _resolvedData$targetF4.map(obj => obj.id.toString())) || [];
126
+
127
+ if (connectedIds.length > 0) {
128
+ // Query relationship items from the database.
129
+ // Therefore, we can have other fields to record in the monitoring field
130
+ const sfToConnect = await context.db[targetListName].findMany({
131
+ where: {
132
+ id: {
133
+ in: connectedIds
134
+ }
135
+ }
136
+ }); // Database query results are not sorted.
137
+ // We need to sort them by ourselves.
138
+
139
+ for (let i = 0; i < connectedIds.length; i++) {
140
+ const sf = sfToConnect.find(obj => {
141
+ return `${obj.id}` === connectedIds[i];
142
+ });
143
+
144
+ if (sf) {
145
+ currentOrder.push({
146
+ id: sf.id.toString(),
147
+ [targetListLabelField]: sf[targetListLabelField]
148
+ });
149
+ }
150
+ }
151
+ } // records the order in the monitoring field
152
+
153
+
154
+ resolvedData[fieldName] = currentOrder;
155
+ }
156
+ }
157
+
158
+ return resolvedData;
159
+ };
160
+
161
+ return list;
162
+ }
163
+ /**
164
+ * This functiona adds the virtual field onto Keystone6 `list` object.
165
+ * For instance, if we want to use monitoring field `manualOrderOfAuthors`
166
+ * to monitor relationship field `authors` in the `post` list object.
167
+ *
168
+ * We could write
169
+ * ```
170
+ * addVirtualFieldToReturnItemsInInputOrder(post, {
171
+ * fieldName: 'manualOrderOfAuthors', // monitoring field
172
+ * targetFieldName: 'authors', // monitored field
173
+ * targetListName: 'Author' // relationship list
174
+ * })
175
+ * ```
176
+ * after executing,
177
+ * `post` list will have `authorsInInputOrder` virtual field.
178
+ *
179
+ * Return value of this virtual field will follow
180
+ * `graphql.list(lists.Author.types.output)` GraphQL schema.
181
+ *
182
+ * And the GQL resolver will be defined in `resolve` function.
183
+ */
184
+
185
+
186
+ function addVirtualFieldToReturnItemsInInputOrder(list, manualOrderField) {
187
+ const virtualFieldName = `${manualOrderField.targetFieldName}InInputOrder`;
188
+ list.fields[virtualFieldName] = (0, _fields.virtual)({
189
+ field: lists => {
190
+ var _lists$manualOrderFie;
191
+
192
+ return _core.graphql.field({
193
+ type: _core.graphql.list(lists === null || lists === void 0 ? void 0 : (_lists$manualOrderFie = lists[manualOrderField.targetListName]) === null || _lists$manualOrderFie === void 0 ? void 0 : _lists$manualOrderFie.types.output),
194
+
195
+ async resolve(item, args, context) {
196
+ var _context$db;
197
+
198
+ const manualOrderFieldValue = (item === null || item === void 0 ? void 0 : item[manualOrderField.fieldName]) || [];
199
+
200
+ if (!Array.isArray(manualOrderFieldValue)) {
201
+ return [];
202
+ } // collect ids from relationship items
203
+
204
+
205
+ const ids = manualOrderFieldValue.map(value => value.id); // query items from database
206
+
207
+ const unorderedItems = await ((_context$db = context.db) === null || _context$db === void 0 ? void 0 : _context$db[manualOrderField.targetListName].findMany({
208
+ where: {
209
+ id: {
210
+ in: ids
211
+ }
212
+ }
213
+ }));
214
+ const orderedItems = []; // sort items according to input order
215
+
216
+ manualOrderFieldValue.forEach(value => {
217
+ const writer = unorderedItems.find(ui => `${ui === null || ui === void 0 ? void 0 : ui.id}` === `${value === null || value === void 0 ? void 0 : value.id}`);
218
+
219
+ if (writer) {
220
+ orderedItems.push(writer);
221
+ }
222
+ });
223
+ return orderedItems;
224
+ }
225
+
226
+ });
227
+ },
228
+ ui: {
229
+ // keystone somehow needs `ui.query` even we "hidden" the field in the next two lines.
230
+ query: `{ id, ${manualOrderField.targetFieldName} }`,
231
+ itemView: {
232
+ fieldMode: 'hidden'
233
+ },
234
+ createView: {
235
+ fieldMode: 'hidden'
236
+ }
237
+ }
238
+ });
239
+ }
240
+
241
+ var _default = addManualOrderRelationshipFields;
242
+ exports.default = _default;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@kids-reporter/cms-core",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "main": "lib/index.js",
6
+ "types": "src/index.ts",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1",
9
+ "build": "make build",
10
+ "clean": "make clean"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/kids-reporter/cms.git",
15
+ "directory": "packages/core"
16
+ },
17
+ "keywords": [
18
+ "KeystoneJS",
19
+ "Keystone 6",
20
+ "CMS",
21
+ "Content Management System",
22
+ "Rich text editor",
23
+ "react",
24
+ "draft-js"
25
+ ],
26
+ "license": "MIT",
27
+ "peerDependencies": {
28
+ "@keystone-6/core": "5.2.0"
29
+ },
30
+ "dependencies": {
31
+ "@google-cloud/storage": "^5.18.0",
32
+ "@keystone-ui/button": "^7.0.2",
33
+ "@keystone-ui/fields": "^7.2.0",
34
+ "@keystone-ui/modals": "^6.0.3",
35
+ "@twreporter/errors": "^1.1.1",
36
+ "@kids-reporter/draft-editor": "0.1.0",
37
+ "axios": "^0.26.0",
38
+ "draft-convert": "^2.1.12",
39
+ "draft-js": "^0.11.7",
40
+ "htmlparser2": "^7.2.0",
41
+ "immutable": "^4.0.0",
42
+ "lodash": "^4.17.21",
43
+ "shortid": "^2.2.16",
44
+ "styled-components": "5.3.5",
45
+ "zlib": "^1.0.5"
46
+ },
47
+ "files": [
48
+ "lib",
49
+ "src",
50
+ "types"
51
+ ]
52
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ BaseListTypeInfo,
3
+ JSONValue,
4
+ FieldTypeFunc,
5
+ CommonFieldConfig,
6
+ jsonFieldTypePolyfilledForSQLite,
7
+ } from '@keystone-6/core/types'
8
+ import { graphql } from '@keystone-6/core'
9
+
10
+ export type JsonFieldConfig<
11
+ ListTypeInfo extends BaseListTypeInfo
12
+ > = CommonFieldConfig<ListTypeInfo> & {
13
+ defaultValue?: JSONValue
14
+ db?: { map?: string }
15
+ disabledButtons: string[]
16
+ }
17
+
18
+ export const richTextEditor = <ListTypeInfo extends BaseListTypeInfo>({
19
+ defaultValue = null,
20
+ disabledButtons = [],
21
+ ...config
22
+ }: JsonFieldConfig<ListTypeInfo> = {}): FieldTypeFunc<ListTypeInfo> => (
23
+ meta
24
+ ) => {
25
+ if ((config as any).isIndexed === 'unique') {
26
+ throw Error(
27
+ "isIndexed: 'unique' is not a supported option for field type textEditor"
28
+ )
29
+ }
30
+
31
+ const resolve = (val: JSONValue | undefined) =>
32
+ val === null && meta.provider === 'postgresql' ? 'DbNull' : val
33
+
34
+ return jsonFieldTypePolyfilledForSQLite(
35
+ meta.provider,
36
+ {
37
+ ...config,
38
+ input: {
39
+ create: {
40
+ arg: graphql.arg({ type: graphql.JSON }),
41
+ resolve(val) {
42
+ return resolve(val === undefined ? defaultValue : val)
43
+ },
44
+ },
45
+ update: { arg: graphql.arg({ type: graphql.JSON }), resolve },
46
+ },
47
+ output: graphql.field({ type: graphql.JSON }),
48
+ views: `@kids-reporter/cms-core/lib/custom-fields/rich-text-editor/views/index`,
49
+ getAdminMeta: () => ({ defaultValue, disabledButtons }),
50
+ },
51
+ {
52
+ default:
53
+ defaultValue === null
54
+ ? undefined
55
+ : {
56
+ kind: 'literal',
57
+ value: JSON.stringify(defaultValue),
58
+ },
59
+ map: config.db?.map,
60
+ }
61
+ )
62
+ }
@@ -0,0 +1,101 @@
1
+ import React from 'react'
2
+ import { jsx, Stack } from '@keystone-ui/core'; // eslint-disable-line
3
+ import { FieldContainer, FieldLabel } from '@keystone-ui/fields'
4
+ import {
5
+ CardValueComponent,
6
+ CellComponent,
7
+ FieldController,
8
+ FieldControllerConfig,
9
+ FieldProps,
10
+ JSONValue,
11
+ } from '@keystone-6/core/types'
12
+ import { CellContainer, CellLink } from '@keystone-6/core/admin-ui/components'
13
+ import { EditorState, convertFromRaw, convertToRaw } from 'draft-js'
14
+ // import { RichTextEditor, decorators } from '@mirrormedia/lilith-draft-editor'
15
+ import de from '@kids-reporter/draft-editor'
16
+
17
+ const { RichTextEditor, decorators } = de.DraftEditor
18
+ export const Field = ({
19
+ field,
20
+ value,
21
+ onChange,
22
+ autoFocus, // eslint-disable-line
23
+ }: FieldProps<typeof controller>) => {
24
+ return (
25
+ <FieldContainer>
26
+ <FieldLabel>
27
+ {field.label}
28
+ <Stack>
29
+ <RichTextEditor
30
+ disabledButtons={field.disabledButtons}
31
+ editorState={value}
32
+ onChange={(editorState) => onChange?.(editorState)}
33
+ />
34
+ </Stack>
35
+ </FieldLabel>
36
+ </FieldContainer>
37
+ )
38
+ }
39
+
40
+ export const Cell: CellComponent = ({ item, field, linkTo }) => {
41
+ const value = item[field.path] + ''
42
+ return linkTo ? (
43
+ <CellLink {...linkTo}>{value}</CellLink>
44
+ ) : (
45
+ <CellContainer>{value}</CellContainer>
46
+ )
47
+ }
48
+ Cell.supportsLinkTo = true
49
+
50
+ export const CardValue: CardValueComponent = ({ item, field }) => {
51
+ return (
52
+ <FieldContainer>
53
+ <FieldLabel>{field.label}</FieldLabel>
54
+ {item[field.path]}
55
+ </FieldContainer>
56
+ )
57
+ }
58
+
59
+ export const controller = (
60
+ config: FieldControllerConfig<{ disabledButtons: string[] }>
61
+ ): FieldController<EditorState, JSONValue> & { disabledButtons: string[] } => {
62
+ return {
63
+ disabledButtons: config.fieldMeta?.disabledButtons ?? [],
64
+ path: config.path,
65
+ label: config.label,
66
+ graphqlSelection: config.path,
67
+ defaultValue: EditorState.createEmpty(decorators),
68
+ deserialize: (data) => {
69
+ const rawContentState = data[config.path]
70
+ if (rawContentState === null) {
71
+ return EditorState.createEmpty(decorators)
72
+ }
73
+ try {
74
+ const contentState = convertFromRaw(rawContentState)
75
+ const editorState = EditorState.createWithContent(
76
+ contentState,
77
+ decorators
78
+ )
79
+ return editorState
80
+ } catch (err) {
81
+ console.error(err)
82
+ return EditorState.createEmpty(decorators)
83
+ }
84
+ },
85
+ serialize: (editorState: EditorState) => {
86
+ if (!editorState) {
87
+ return { [config.path]: null }
88
+ }
89
+
90
+ try {
91
+ const rawContentState = convertToRaw(editorState.getCurrentContent())
92
+ return {
93
+ [config.path]: rawContentState,
94
+ }
95
+ } catch (err) {
96
+ console.error(err)
97
+ return { [config.path]: null }
98
+ }
99
+ },
100
+ }
101
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import addManualOrderRelationshipFields from './utils/manual-order-relationship'
2
+ import draftConverter from '@kids-reporter/draft-editor/lib/draft-converter'
3
+ import { richTextEditor } from './custom-fields/rich-text-editor'
4
+
5
+ export const customFields = {
6
+ draftConverter,
7
+ richTextEditor,
8
+ }
9
+
10
+ export const utils = {
11
+ addManualOrderRelationshipFields,
12
+ }
13
+
14
+ export default {
15
+ customFields,
16
+ utils,
17
+ }
@@ -0,0 +1,233 @@
1
+ import { BaseItem } from '@keystone-6/core/types'
2
+ import { ListConfig, graphql } from '@keystone-6/core'
3
+ import { json, virtual } from '@keystone-6/core/fields'
4
+
5
+ type ManualOrderFieldConfig = {
6
+ fieldName: string
7
+ fieldLabel?: string
8
+ targetFieldName: string
9
+ targetListName: string
10
+ targetListLabelField: string
11
+ }
12
+
13
+ /**
14
+ * For `relationship` field, KeystoneJS won't take user input order into account.
15
+ * Therefore, after the create/update operation is done,
16
+ * the order of relationship items maybe not be the same order as the user input order.
17
+ *
18
+ * This function
19
+ * - adds monitoring fields in the list
20
+ * - adds virtual fields in the list. These virtual fields could return relationship items in order.
21
+ * - decorate `list.hooks.resolveInput` to record the user input order in the monitoring fields
22
+ *
23
+ * For example, if we have two lists like
24
+ * ```
25
+ * const User = {
26
+ * fields: {
27
+ * name: text(),
28
+ * }
29
+ * }
30
+ *
31
+ * const Post = {
32
+ * fields: {
33
+ * title: text(),
34
+ * content: text(),
35
+ * authors: relationship({ref: 'User', many: true})
36
+ * }
37
+ * }
38
+ * ```
39
+ *
40
+ * if we want to snapshot the authors input order, we can use this function like
41
+ * ```
42
+ * const postList = addManualOrderRelationshipFields([
43
+ * {
44
+ * fieldName: 'manualOrderOfAuthors',
45
+ * fieldLabel: 'authors 手動排序結果',
46
+ * targetFieldName: 'authors', // the target field to record the user input order
47
+ * targetListName: 'User', // relationship list name
48
+ * targetListLabelField: 'name', // refer to `User.fields.name`
49
+ * }
50
+ * ])(Post)
51
+ * ```
52
+ *
53
+ * `addManualOrderRelationshipFields` will create another JSON field `manualOrderOfAuthors` and virtual field `authorsInInputOrder` in the list,
54
+ * and decorate `list.hooks.resolveInput` to record the update/create operation,
55
+ * if the operation modifies the order of the relationship field.
56
+ *
57
+ * `authorsInInputOrder` is a virtual field, which means its value is computed on-the-fly, not stored in the database. This virtual field combines relationship field `authors` and monitoring field `manualOrderOfAuthors` to sort the authors in specific input order.
58
+ */
59
+ function addManualOrderRelationshipFields(
60
+ manualOrderFields: ManualOrderFieldConfig[] = [],
61
+ list: ListConfig<any, any>
62
+ ) {
63
+ manualOrderFields.forEach((mo) => {
64
+ if (!list.fields?.[mo.fieldName]) {
65
+ list.fields[mo.fieldName] = json({
66
+ label: mo.fieldLabel,
67
+ ui: {
68
+ itemView: {
69
+ fieldMode: 'read',
70
+ },
71
+ },
72
+ })
73
+ }
74
+
75
+ // add virtual field definition
76
+ addVirtualFieldToReturnItemsInInputOrder(list, mo)
77
+ })
78
+
79
+ // decorate `resolveInput` hook
80
+ list.hooks = list.hooks || {}
81
+ const originResolveInput = list.hooks?.resolveInput
82
+ list.hooks.resolveInput = async (props) => {
83
+ let resolvedData = props.resolvedData
84
+
85
+ if (typeof originResolveInput === 'function') {
86
+ resolvedData = await originResolveInput(props)
87
+ }
88
+
89
+ const { item, context } = props
90
+
91
+ // check if create/update item has the fields
92
+ // we want to monitor
93
+ for (let i = 0; i < manualOrderFields.length; i++) {
94
+ const {
95
+ targetFieldName,
96
+ fieldName,
97
+ targetListName,
98
+ targetListLabelField,
99
+ } = manualOrderFields[i]
100
+
101
+ // if create/update operation creates/modifies the `${targetFieldName}` field
102
+ if (resolvedData?.[targetFieldName]) {
103
+ let currentOrder: { id: string }[] = []
104
+
105
+ // update operation due to `item` not being `undefiend`
106
+ if (item) {
107
+ const previousOrder: { id: string }[] = Array.isArray(item[fieldName])
108
+ ? item[fieldName]
109
+ : []
110
+
111
+ // user disconnects/removes some relationship items.
112
+ const disconnectIds =
113
+ resolvedData[
114
+ targetFieldName
115
+ ]?.disconnect?.map((obj: { id: number }) => obj.id.toString()) || []
116
+
117
+ // filtered out to-be-disconnected relationship items
118
+ currentOrder = previousOrder.filter(({ id }: { id: string }) => {
119
+ return disconnectIds.indexOf(id) === -1
120
+ })
121
+ }
122
+
123
+ // user connects/adds some relationship item.
124
+ const connectedIds =
125
+ resolvedData[targetFieldName]?.connect?.map((obj: { id: number }) =>
126
+ obj.id.toString()
127
+ ) || []
128
+
129
+ if (connectedIds.length > 0) {
130
+ // Query relationship items from the database.
131
+ // Therefore, we can have other fields to record in the monitoring field
132
+ const sfToConnect = await context.db[targetListName].findMany({
133
+ where: { id: { in: connectedIds } },
134
+ })
135
+
136
+ // Database query results are not sorted.
137
+ // We need to sort them by ourselves.
138
+ for (let i = 0; i < connectedIds.length; i++) {
139
+ const sf = sfToConnect.find((obj) => {
140
+ return `${obj.id}` === connectedIds[i]
141
+ })
142
+ if (sf) {
143
+ currentOrder.push({
144
+ id: sf.id.toString(),
145
+ [targetListLabelField]: sf[targetListLabelField],
146
+ })
147
+ }
148
+ }
149
+ }
150
+
151
+ // records the order in the monitoring field
152
+ resolvedData[fieldName] = currentOrder
153
+ }
154
+ }
155
+ return resolvedData
156
+ }
157
+ return list
158
+ }
159
+
160
+ /**
161
+ * This functiona adds the virtual field onto Keystone6 `list` object.
162
+ * For instance, if we want to use monitoring field `manualOrderOfAuthors`
163
+ * to monitor relationship field `authors` in the `post` list object.
164
+ *
165
+ * We could write
166
+ * ```
167
+ * addVirtualFieldToReturnItemsInInputOrder(post, {
168
+ * fieldName: 'manualOrderOfAuthors', // monitoring field
169
+ * targetFieldName: 'authors', // monitored field
170
+ * targetListName: 'Author' // relationship list
171
+ * })
172
+ * ```
173
+ * after executing,
174
+ * `post` list will have `authorsInInputOrder` virtual field.
175
+ *
176
+ * Return value of this virtual field will follow
177
+ * `graphql.list(lists.Author.types.output)` GraphQL schema.
178
+ *
179
+ * And the GQL resolver will be defined in `resolve` function.
180
+ */
181
+ function addVirtualFieldToReturnItemsInInputOrder(
182
+ list: ListConfig<any, any>,
183
+ manualOrderField: ManualOrderFieldConfig
184
+ ) {
185
+ const virtualFieldName = `${manualOrderField.targetFieldName}InInputOrder`
186
+ list.fields[virtualFieldName] = virtual({
187
+ field: (lists) => {
188
+ return graphql.field({
189
+ type: graphql.list(
190
+ lists?.[manualOrderField.targetListName]?.types.output
191
+ ),
192
+ async resolve(item: Record<string, unknown>, args, context) {
193
+ const manualOrderFieldValue = item?.[manualOrderField.fieldName] || []
194
+ if (!Array.isArray(manualOrderFieldValue)) {
195
+ return []
196
+ }
197
+
198
+ // collect ids from relationship items
199
+ const ids = manualOrderFieldValue.map((value) => value.id)
200
+
201
+ // query items from database
202
+ const unorderedItems = await context.db?.[
203
+ manualOrderField.targetListName
204
+ ].findMany({
205
+ where: { id: { in: ids } },
206
+ })
207
+
208
+ const orderedItems: BaseItem[] = []
209
+
210
+ // sort items according to input order
211
+ manualOrderFieldValue.forEach((value) => {
212
+ const writer = unorderedItems.find(
213
+ (ui) => `${ui?.id}` === `${value?.id}`
214
+ )
215
+ if (writer) {
216
+ orderedItems.push(writer)
217
+ }
218
+ })
219
+
220
+ return orderedItems
221
+ },
222
+ })
223
+ },
224
+ ui: {
225
+ // keystone somehow needs `ui.query` even we "hidden" the field in the next two lines.
226
+ query: `{ id, ${manualOrderField.targetFieldName} }`,
227
+ itemView: { fieldMode: 'hidden' },
228
+ createView: { fieldMode: 'hidden' },
229
+ },
230
+ })
231
+ }
232
+
233
+ export default addManualOrderRelationshipFields