@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 +42 -0
- package/lib/custom-fields/rich-text-editor/index.js +61 -0
- package/lib/custom-fields/rich-text-editor/views/index.js +115 -0
- package/lib/index.js +29 -0
- package/lib/utils/manual-order-relationship.js +242 -0
- package/package.json +52 -0
- package/src/custom-fields/rich-text-editor/index.ts +62 -0
- package/src/custom-fields/rich-text-editor/views/index.tsx +101 -0
- package/src/index.ts +17 -0
- package/src/utils/manual-order-relationship.ts +233 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# [@mirrormedia/lilith-core](https://www.npmjs.com/package/@mirrormedia/lilith-core) · 
|
|
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
|