@01-edu/shared 1.0.6 → 1.0.12
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 +27 -1
- package/attrs-defs.js +412 -115
- package/attrs.js +102 -15
- package/bin/check-definitions.js +8 -0
- package/definitions-checker.js +25 -23
- package/onboarding.js +1 -0
- package/package.json +2 -2
- package/path.js +18 -8
- package/toolbox.js +32 -9
package/attrs.js
CHANGED
|
@@ -12,12 +12,17 @@ const typeCheckers = {
|
|
|
12
12
|
array: Array.isArray,
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const determinType = value => {
|
|
16
|
+
if (Array.isArray(value)) return 'array'
|
|
17
|
+
if (typeof value === 'object' && value !== null) return value.type
|
|
18
|
+
return typeof value
|
|
19
|
+
}
|
|
15
20
|
const typeChecker = (defs, value, object, key) => {
|
|
16
21
|
const { type, check, options } = defs
|
|
17
22
|
|
|
18
23
|
if (value == null) {
|
|
19
|
-
if (!defs.required) return true
|
|
20
|
-
// if no value for required attribute, reject
|
|
24
|
+
if (!defs.required || defs.value !== undefined) return true
|
|
25
|
+
// if no value for required attribute without a default value, reject
|
|
21
26
|
throw Error(`missing value for required attribute ${key}`)
|
|
22
27
|
}
|
|
23
28
|
|
|
@@ -33,11 +38,17 @@ const typeChecker = (defs, value, object, key) => {
|
|
|
33
38
|
|
|
34
39
|
if (options) {
|
|
35
40
|
const opts = typeof options === 'function' ? options(object) : options
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
if (opts.length === 1) {
|
|
42
|
+
if (opts[0] !== value) {
|
|
43
|
+
throw Error(`${key} must be ${opts[0]} but was ${value}`)
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
const isAnOption = opts.includes(value)
|
|
47
|
+
if (!isAnOption) {
|
|
48
|
+
throw Error(
|
|
49
|
+
`invalid option for ${key}: should be included in ${opts.join(', ')}`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
|
|
@@ -55,16 +66,23 @@ const typeChecker = (defs, value, object, key) => {
|
|
|
55
66
|
// every value have to match one of the type definition
|
|
56
67
|
|
|
57
68
|
const uniqueDef = type.length === 1 && type[0]
|
|
69
|
+
// convert array type into object for better accessibility
|
|
58
70
|
const types =
|
|
59
71
|
!uniqueDef &&
|
|
60
|
-
Object.fromEntries(
|
|
72
|
+
Object.fromEntries(
|
|
73
|
+
type.map(t => [
|
|
74
|
+
Array.isArray(t.type) ? 'array' : t.type?.type?.value || t.type,
|
|
75
|
+
t,
|
|
76
|
+
]),
|
|
77
|
+
)
|
|
78
|
+
|
|
61
79
|
for (const [index, v] of value.entries()) {
|
|
62
80
|
const err = Error('checks failed for all types')
|
|
63
81
|
err.index = index
|
|
64
82
|
err.key = key
|
|
65
83
|
err.label = defs.label
|
|
66
|
-
const subdefs =
|
|
67
|
-
|
|
84
|
+
const subdefs = uniqueDef || types[determinType(v)]
|
|
85
|
+
|
|
68
86
|
if (!subdefs) {
|
|
69
87
|
err.details = {
|
|
70
88
|
label: 'Unknown structure',
|
|
@@ -79,7 +97,6 @@ const typeChecker = (defs, value, object, key) => {
|
|
|
79
97
|
label: subdefs.label || error.label,
|
|
80
98
|
err: error,
|
|
81
99
|
}
|
|
82
|
-
|
|
83
100
|
throw err
|
|
84
101
|
}
|
|
85
102
|
}
|
|
@@ -111,7 +128,7 @@ const typeChecker = (defs, value, object, key) => {
|
|
|
111
128
|
// same as attrs with the check & function by name generated and descriptions form markdown files
|
|
112
129
|
export const attributes = mapEntries(attrs, ([attrKey, matches]) => [
|
|
113
130
|
attrKey,
|
|
114
|
-
mapValues(matches, (defs
|
|
131
|
+
mapValues(matches, (defs /* type */) => ({
|
|
115
132
|
...defs,
|
|
116
133
|
check: (value, object) => typeChecker(defs, value, object, attrKey),
|
|
117
134
|
})),
|
|
@@ -121,8 +138,8 @@ export const relationAttributes = mapEntries(
|
|
|
121
138
|
relationAttrs,
|
|
122
139
|
([attrKey, byParent]) => [
|
|
123
140
|
attrKey,
|
|
124
|
-
mapValues(byParent, (matches
|
|
125
|
-
mapValues(matches, (defs
|
|
141
|
+
mapValues(byParent, (matches /* parentType */) =>
|
|
142
|
+
mapValues(matches, (defs /* childType */) => ({
|
|
126
143
|
...defs,
|
|
127
144
|
check: (value, object) => typeChecker(defs, value, object, attrKey),
|
|
128
145
|
})),
|
|
@@ -130,6 +147,68 @@ export const relationAttributes = mapEntries(
|
|
|
130
147
|
],
|
|
131
148
|
)
|
|
132
149
|
|
|
150
|
+
// white list of attributes that can be applied in bulk
|
|
151
|
+
const allowedBulkAttrs = {
|
|
152
|
+
codeEditor: { enabled: {} },
|
|
153
|
+
validations: {
|
|
154
|
+
// this is for the different options of validation,
|
|
155
|
+
// we know that raids for now is the only one that contains multiple validations
|
|
156
|
+
// and we **don't** want admin_selection
|
|
157
|
+
type: [
|
|
158
|
+
'admin_audit',
|
|
159
|
+
'tester',
|
|
160
|
+
'dedicated_auditors_for_event',
|
|
161
|
+
'user_audit',
|
|
162
|
+
],
|
|
163
|
+
ratio: {},
|
|
164
|
+
required: {},
|
|
165
|
+
matchInfluence: {},
|
|
166
|
+
cooldown: {},
|
|
167
|
+
preQuestions: {},
|
|
168
|
+
postQuestions: {},
|
|
169
|
+
matchWhere: {},
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
export const getAllBulkAttrs = (parentType, childType) => {
|
|
173
|
+
const relationAttrs = getDefaultRelAttrs(parentType, childType)
|
|
174
|
+
const attrs = getDefaultAttrs({ type: childType })
|
|
175
|
+
const allAttrs = { ...relationAttrs, ...attrs }
|
|
176
|
+
return filterBulkAttrs(allAttrs, allowedBulkAttrs)
|
|
177
|
+
}
|
|
178
|
+
export const filterBulkAttrs = (attrs, allowedAttrs) => {
|
|
179
|
+
if (!attrs) return {}
|
|
180
|
+
if (Array.isArray(attrs.type)) {
|
|
181
|
+
const { type, ...rest } = attrs
|
|
182
|
+
// if it is type array and from the validation attribute we need to filter the validation type
|
|
183
|
+
const types = type
|
|
184
|
+
.map(a => filterBulkAttrs(a, allowedAttrs))
|
|
185
|
+
.filter(({ type }) => allowedAttrs.type.includes(type.type.value))
|
|
186
|
+
return { ...rest, type: types }
|
|
187
|
+
}
|
|
188
|
+
if (attrs.type && typeof attrs.type === 'object') {
|
|
189
|
+
const { type, ...rest } = attrs
|
|
190
|
+
const filtered = { type: {}, ...rest }
|
|
191
|
+
const typesKeys = Object.keys(type)
|
|
192
|
+
for (const t of typesKeys) {
|
|
193
|
+
if (allowedAttrs?.[t]) {
|
|
194
|
+
filtered.type[t] = type[t]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return filtered
|
|
198
|
+
}
|
|
199
|
+
if (typeof attrs === 'object' && !attrs.type) {
|
|
200
|
+
const filtered = {}
|
|
201
|
+
const keys = Object.keys(attrs)
|
|
202
|
+
for (const k of keys) {
|
|
203
|
+
if (allowedAttrs[k]) {
|
|
204
|
+
filtered[k] = filterBulkAttrs(attrs[k], allowedAttrs[k])
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return filtered
|
|
208
|
+
}
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
133
212
|
// map from attrs[name][type] to attrs[type][name]
|
|
134
213
|
export const attrsByType = {}
|
|
135
214
|
|
|
@@ -141,7 +220,7 @@ for (const [name, matches] of Object.entries(attributes)) {
|
|
|
141
220
|
// handle translations: generate translation attrs and required status
|
|
142
221
|
const { label, ...restDefs } = defs
|
|
143
222
|
if (defs.functionsByName?.translate) {
|
|
144
|
-
// perf measures done:
|
|
223
|
+
// perf measures done: increase from 0.671ms loadtime to 13.261ms
|
|
145
224
|
// should not impact the perfs
|
|
146
225
|
for (const [code, language] of languagesEntries) {
|
|
147
226
|
const newLabel = `${label} - ${language}`
|
|
@@ -259,6 +338,14 @@ const expandAttr = (key, value, defs, object, getUser) => {
|
|
|
259
338
|
|
|
260
339
|
export const expandAttrs = (object, getUser) => {
|
|
261
340
|
if (!object.children) return (object.children = {})
|
|
341
|
+
|
|
342
|
+
for (const [key, defs] of getDefaultAttrsEntries(object)) {
|
|
343
|
+
if (object.attrs[key] === null) {
|
|
344
|
+
console.warn(`value is null for ${object.name} - ${key} - ${object.id}`)
|
|
345
|
+
}
|
|
346
|
+
expandAttr(key, object.attrs, defs, object, getUser)
|
|
347
|
+
}
|
|
348
|
+
|
|
262
349
|
let prev
|
|
263
350
|
for (const child of Object.values(object.children)) {
|
|
264
351
|
child.parent = object
|
package/bin/check-definitions.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readFile, stat, watch } from 'node:fs/promises'
|
|
|
4
4
|
|
|
5
5
|
import { checkAndBuildDefinitions } from '../definitions-checker.js'
|
|
6
6
|
|
|
7
|
+
const rootTypes = ['module', 'piscine', 'signup', 'onboarding']
|
|
7
8
|
const isAudit = validation => validation.type.endsWith('_audit')
|
|
8
9
|
const readDef = async key => {
|
|
9
10
|
const path = key == null ? 'content/def.json' : `content/${key}/def.json`
|
|
@@ -11,6 +12,12 @@ const readDef = async key => {
|
|
|
11
12
|
def.attrs || (def.attrs = {})
|
|
12
13
|
def.referencePath = path
|
|
13
14
|
|
|
15
|
+
if (key == null && !rootTypes.includes(def.type)) {
|
|
16
|
+
throw Error(
|
|
17
|
+
`Root definition must be one of ${rootTypes.join(', ')}, found: ${def.type}`,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
switch (def.type) {
|
|
15
22
|
case 'project':
|
|
16
23
|
// biome-ignore lint/suspicious/noFallthroughSwitchClause: We want to fallthrough
|
|
@@ -54,6 +61,7 @@ if (process.argv.includes('--watch') || process.argv.includes('-w')) {
|
|
|
54
61
|
await runChecks()
|
|
55
62
|
for await (const event of watch('.', { recursive: true })) {
|
|
56
63
|
console.clear()
|
|
64
|
+
if (!event.filename.endsWith('def.json')) continue
|
|
57
65
|
console.log(event.eventType, 'on', event.filename, '\n')
|
|
58
66
|
await runChecks()
|
|
59
67
|
}
|
package/definitions-checker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { childTypes, objectTypes } from './toolbox.js'
|
|
2
1
|
import { attributes, relationAttributes } from './attrs.js'
|
|
2
|
+
import { childTypes, objectTypes } from './toolbox.js'
|
|
3
3
|
|
|
4
4
|
const normalize = str =>
|
|
5
5
|
str
|
|
@@ -9,16 +9,8 @@ const normalize = str =>
|
|
|
9
9
|
.replaceAll(' ', '-')
|
|
10
10
|
|
|
11
11
|
const assertDef = def => {
|
|
12
|
-
const {
|
|
13
|
-
|
|
14
|
-
name,
|
|
15
|
-
attrs,
|
|
16
|
-
children,
|
|
17
|
-
childrenAttrs,
|
|
18
|
-
refId,
|
|
19
|
-
referencePath,
|
|
20
|
-
...rest
|
|
21
|
-
} = def
|
|
12
|
+
const { type, name, attrs, children, childrenAttrs, referencePath, ...rest } =
|
|
13
|
+
def
|
|
22
14
|
const [extra] = Object.keys(rest)
|
|
23
15
|
if (extra) {
|
|
24
16
|
throw Error(
|
|
@@ -27,11 +19,12 @@ const assertDef = def => {
|
|
|
27
19
|
}
|
|
28
20
|
if (!objectTypes.has(type)) throw Error(`Invalid type property`)
|
|
29
21
|
if (!name || typeof name !== 'string') throw Error(`Invalid name property`)
|
|
30
|
-
if (attrs
|
|
31
|
-
|
|
22
|
+
if (!attrs || typeof attrs !== 'object' || Array.isArray(attrs)) {
|
|
23
|
+
throw Error(`Invalid attrs property`)
|
|
24
|
+
}
|
|
32
25
|
if (childrenAttrs) throw Error(`childrenAttrs is no longer supported`)
|
|
33
26
|
|
|
34
|
-
for (const key of Object.keys(attrs)) {
|
|
27
|
+
for (const key of Object.keys(attrs || {})) {
|
|
35
28
|
if (relationAttributes[key]) {
|
|
36
29
|
throw Error(
|
|
37
30
|
`Attr ${key} should be defined in the relation with its parent, not on the object itself.`,
|
|
@@ -52,16 +45,19 @@ const assertDef = def => {
|
|
|
52
45
|
}
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
const buildTree = (
|
|
49
|
+
{ name, type, attrs, children, referencePath },
|
|
50
|
+
parent,
|
|
51
|
+
depth,
|
|
52
|
+
) => {
|
|
53
|
+
// As we check for the parent / child type structure, this should never happen
|
|
54
|
+
// so we did not bother to make a more descriptive error for this case
|
|
55
|
+
if (depth > 100) throw Error('Unexpected very deep tree, maybe circular ?')
|
|
60
56
|
const object = { name, type, attrs, children: {}, referencePath, parent }
|
|
61
57
|
if (!children) return object
|
|
62
58
|
let prev
|
|
63
59
|
for (const [key, { ref, ...rest }] of Object.entries(children)) {
|
|
64
|
-
const child = buildTree(ref, object)
|
|
60
|
+
const child = buildTree(ref, object, (depth || 0) + 1)
|
|
65
61
|
child.attrs = { ...child.attrs, ...rest }
|
|
66
62
|
prev && (prev.next = child)
|
|
67
63
|
child.prev = prev
|
|
@@ -73,7 +69,9 @@ const buildTree = ({ name, type, attrs, children, referencePath }, parent) => {
|
|
|
73
69
|
const assertRelation = (parent, key, relation) => {
|
|
74
70
|
if (!/^[a-z0-9-]*$/.test(key)) {
|
|
75
71
|
throw Error(
|
|
76
|
-
`Invalid key for child ${key} Kebab-case key suggestion: ${normalize(
|
|
72
|
+
`Invalid key for child ${key} Kebab-case key suggestion: ${normalize(
|
|
73
|
+
key,
|
|
74
|
+
)}`,
|
|
77
75
|
)
|
|
78
76
|
}
|
|
79
77
|
|
|
@@ -176,7 +174,10 @@ export const checkAndBuildDefinitions = async readDef => {
|
|
|
176
174
|
const getDefs = async ([key, relation]) => {
|
|
177
175
|
let def
|
|
178
176
|
try {
|
|
179
|
-
|
|
177
|
+
const cached = cache[key]
|
|
178
|
+
def = await (cached || (cache[key] = readDef(key)))
|
|
179
|
+
relation && (relation.ref = def)
|
|
180
|
+
if (cached) return [] // skip assert and children checks if already cached
|
|
180
181
|
} catch (err) {
|
|
181
182
|
err.name = key
|
|
182
183
|
throw err
|
|
@@ -188,11 +189,12 @@ export const checkAndBuildDefinitions = async readDef => {
|
|
|
188
189
|
err.referencePath = def.referencePath
|
|
189
190
|
throw err
|
|
190
191
|
}
|
|
191
|
-
relation && (relation.ref = def)
|
|
192
192
|
const relations = Object.entries(def.children || {}).map(getDefs)
|
|
193
193
|
try {
|
|
194
194
|
return [def, ...(await Promise.all(relations)).flat()]
|
|
195
195
|
} catch (err) {
|
|
196
|
+
// Some content may be used at multiple place (ex: exam exercises)
|
|
197
|
+
// so we may have multiple parents
|
|
196
198
|
key && (err.parents || (err.parents = [])).push(key)
|
|
197
199
|
throw err
|
|
198
200
|
}
|
package/onboarding.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@01-edu/shared",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "",
|
|
6
6
|
"scripts": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"./skill-definitions.js",
|
|
22
22
|
"./toolbox.js"
|
|
23
23
|
],
|
|
24
|
-
"license": "
|
|
24
|
+
"license": "Fair",
|
|
25
25
|
"bin": {
|
|
26
26
|
"check-defs": "./bin/check-definitions.js"
|
|
27
27
|
},
|
package/path.js
CHANGED
|
@@ -39,15 +39,23 @@ const walk = (object, segment) => {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* @
|
|
43
|
-
* @
|
|
44
|
-
* @
|
|
42
|
+
* @param {string} relativePath - a relative path
|
|
43
|
+
* @param {Object} currentObj - the object from where the relative path starts
|
|
44
|
+
* @param {Object} [opts = { throwError: true } ] - options object with boolean property `throwError` to throw an error in case the path is invalid or no object is found
|
|
45
|
+
* @returns {Object} - the object in the relative path
|
|
45
46
|
*/
|
|
46
|
-
export const getObjectFromRelativePath = (
|
|
47
|
+
export const getObjectFromRelativePath = (
|
|
48
|
+
relativePath,
|
|
49
|
+
currentObj,
|
|
50
|
+
opts = { throwError: true },
|
|
51
|
+
) => {
|
|
47
52
|
const currentPath = currentObj.path
|
|
48
53
|
if (isAbsolutePath(relativePath)) {
|
|
49
|
-
throw Error("'relativePath' must be a relative path")
|
|
54
|
+
if (opts.throwError) throw Error("'relativePath' must be a relative path")
|
|
55
|
+
console.error("'relativePath' must be a relative path")
|
|
56
|
+
return undefined
|
|
50
57
|
}
|
|
58
|
+
|
|
51
59
|
// Ignore trailing `/` in the relative path
|
|
52
60
|
if (relativePath[relativePath.length - 1] === '/') {
|
|
53
61
|
relativePath = relativePath.slice(0, -1)
|
|
@@ -55,9 +63,11 @@ export const getObjectFromRelativePath = (relativePath, currentObj) => {
|
|
|
55
63
|
|
|
56
64
|
const matchingObject = relativePath.split('/').reduce(walk, currentObj)
|
|
57
65
|
if (matchingObject) return matchingObject
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
66
|
+
|
|
67
|
+
const errorMessage = `Incorrect relative path '${relativePath}': no object found — current path = '${currentPath}'`
|
|
68
|
+
if (opts.throwError) throw Error(errorMessage)
|
|
69
|
+
console.error(errorMessage)
|
|
70
|
+
return undefined
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
const isAbsolutePath = path => path.startsWith('/')
|
package/toolbox.js
CHANGED
|
@@ -27,7 +27,13 @@ export const objectTypes = new Set([...contentObjects, ...onboardingTypes])
|
|
|
27
27
|
|
|
28
28
|
export const childTypes = {
|
|
29
29
|
campus: ['signup', 'onboarding', 'piscine', 'module'],
|
|
30
|
-
signup: [
|
|
30
|
+
signup: [
|
|
31
|
+
'form-step',
|
|
32
|
+
'sign-step',
|
|
33
|
+
'upload-step',
|
|
34
|
+
'contact-validation-step',
|
|
35
|
+
'avatar-step',
|
|
36
|
+
],
|
|
31
37
|
onboarding: [
|
|
32
38
|
'games',
|
|
33
39
|
'administration',
|
|
@@ -40,6 +46,7 @@ export const childTypes = {
|
|
|
40
46
|
'sign-step',
|
|
41
47
|
'upload-step',
|
|
42
48
|
'contact-validation-step',
|
|
49
|
+
'avatar-step',
|
|
43
50
|
],
|
|
44
51
|
piscine: ['quest', 'exam', 'raid', 'project'],
|
|
45
52
|
exam: ['exercise'],
|
|
@@ -182,15 +189,17 @@ export const formatedDuration = (seconds, { noSeconds } = {}) => {
|
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
const toDate = date => (date instanceof Date ? date : new Date(date))
|
|
185
|
-
export const toDateFormat = d => {
|
|
192
|
+
export const toDateFormat = (d, isISO = true) => {
|
|
186
193
|
const date = toDate(d)
|
|
187
194
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
188
195
|
const day = date.getDate().toString().padStart(2, '0')
|
|
189
196
|
|
|
190
|
-
return
|
|
197
|
+
return isISO
|
|
198
|
+
? `${date.getFullYear()}-${month}-${day}`
|
|
199
|
+
: `${day}/${month}/${date.getFullYear()}`
|
|
191
200
|
}
|
|
192
201
|
|
|
193
|
-
export const toISOStringWithTimeZone = d => toDate(d).toISOString()
|
|
202
|
+
export const toISOStringWithTimeZone = d => toDate(d).toISOString()
|
|
194
203
|
|
|
195
204
|
export const toDateFormatWithTime = (d, separator = 'T') => {
|
|
196
205
|
const date = toDate(d)
|
|
@@ -220,7 +229,7 @@ export const monthNames = [
|
|
|
220
229
|
'Dec',
|
|
221
230
|
]
|
|
222
231
|
|
|
223
|
-
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
232
|
+
export const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
224
233
|
|
|
225
234
|
const hour = hour => {
|
|
226
235
|
if (hour === 0 || hour === 12) return 12
|
|
@@ -256,12 +265,12 @@ const getDateElems = date => {
|
|
|
256
265
|
return { day, jj, mm, yyyy, hh, min }
|
|
257
266
|
}
|
|
258
267
|
|
|
259
|
-
export const formatedDateTime = date => {
|
|
268
|
+
export const formatedDateTime = (date, splitter = 'at') => {
|
|
260
269
|
if (!date) return '-'
|
|
261
270
|
if (dateIsPermanent(date)) return '∞'
|
|
262
271
|
const { jj, mm, yyyy, hh, min } = getDateElems(date)
|
|
263
272
|
|
|
264
|
-
return `${monthNames[mm]} ${jj}, ${yyyy}
|
|
273
|
+
return `${monthNames[mm]} ${jj}, ${yyyy} ${splitter} ${hour(hh)}:${minPad(
|
|
265
274
|
min,
|
|
266
275
|
)} ${suffix(hh)}`
|
|
267
276
|
}
|
|
@@ -337,13 +346,12 @@ const toSnakeCase = str =>
|
|
|
337
346
|
// id card > id-card
|
|
338
347
|
// should be enough for unique usage we have of it in check-refs
|
|
339
348
|
// checked the perfs and it is faster + more secure than using replace(s) + toLowerCase
|
|
340
|
-
const toLowerCase = x => x.toLowerCase()
|
|
341
349
|
export const toKebabCase = str =>
|
|
342
350
|
str
|
|
343
351
|
.match(
|
|
344
352
|
/[A-Z](?=[A-Z][a-z]+[0-9]*[a-z]|\b)|[A-Z]?[a-z]+[0-9]+[a-z]?[A-Z]*|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g,
|
|
345
353
|
)
|
|
346
|
-
.map(toLowerCase)
|
|
354
|
+
.map(x => x.toLowerCase())
|
|
347
355
|
.join('-')
|
|
348
356
|
|
|
349
357
|
const spaceJoin = (_, a, b) => `${a} ${b}`
|
|
@@ -481,3 +489,18 @@ export const base64Encode = str => {
|
|
|
481
489
|
|
|
482
490
|
export const timePlusDelay = (time, delay) =>
|
|
483
491
|
new Date(new Date(time).getTime() + delay).toISOString()
|
|
492
|
+
|
|
493
|
+
export const getRecordStatus = record => {
|
|
494
|
+
if (!isFinished(record.startAt)) return 'starting soon'
|
|
495
|
+
if (record.endAt && isFinished(record.endAt)) {
|
|
496
|
+
return 'finished'
|
|
497
|
+
}
|
|
498
|
+
if (record.type.isPermanent) return 'permanent'
|
|
499
|
+
return record.endAt ? 'in progress' : 'unblock required'
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export const createFrequencyMap = arr =>
|
|
503
|
+
arr.reduce((map, item) => {
|
|
504
|
+
map[item] = (map[item] || 0) + 1
|
|
505
|
+
return map
|
|
506
|
+
}, {})
|