@budibase/frontend-core 3.5.2 → 3.6.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/package.json +2 -2
- package/src/api/ai.ts +9 -0
- package/src/api/index.ts +2 -0
- package/src/api/oauth2.ts +86 -0
- package/src/api/types.ts +6 -1
- package/src/components/ChangePasswordModal.svelte +43 -0
- package/src/components/PasswordRepeatInput.svelte +54 -0
- package/src/components/ProfileModal.svelte +39 -0
- package/src/components/grid/cells/GutterCell.svelte +10 -6
- package/src/components/grid/cells/HeaderCell.svelte +7 -4
- package/src/components/grid/layout/Grid.svelte +17 -3
- package/src/components/grid/layout/HeaderRow.svelte +1 -1
- package/src/components/grid/layout/NewRow.svelte +12 -2
- package/src/components/grid/layout/StickyColumn.svelte +1 -1
- package/src/components/grid/lib/constants.ts +2 -2
- package/src/components/grid/overlays/ScrollOverlay.svelte +2 -2
- package/src/components/grid/stores/config.ts +9 -2
- package/src/components/grid/stores/index.ts +1 -1
- package/src/components/grid/stores/scroll.ts +8 -4
- package/src/components/index.js +3 -0
- package/src/constants.ts +6 -0
- package/src/fetch/DataFetch.ts +30 -6
- package/src/fetch/TableFetch.ts +3 -1
- package/src/fetch/ViewV2Fetch.ts +4 -3
- package/src/utils/index.ts +1 -0
- package/src/utils/schema.js +3 -1
- package/src/utils/utils.ts +12 -7
- package/src/utils/validation/index.js +2 -0
- package/src/utils/validation/validation.js +28 -0
- package/src/utils/validation/validators.js +18 -0
- package/src/utils/validation/yup/app.js +87 -0
- package/src/utils/validation/yup/index.js +151 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/frontend-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Budibase frontend core libraries used in builder and client",
|
|
5
5
|
"author": "Budibase",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
"shortid": "2.2.15",
|
|
18
18
|
"socket.io-client": "^4.7.5"
|
|
19
19
|
},
|
|
20
|
-
"gitHead": "
|
|
20
|
+
"gitHead": "fcc8291c1f2968ee7fd17bd47ee6ca19181a14a7"
|
|
21
21
|
}
|
package/src/api/ai.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { GenerateJsRequest, GenerateJsResponse } from "@budibase/types"
|
|
1
2
|
import { BaseAPIClient } from "./types"
|
|
2
3
|
|
|
3
4
|
export interface AIEndpoints {
|
|
4
5
|
generateCronExpression: (prompt: string) => Promise<{ message: string }>
|
|
6
|
+
generateJs: (req: GenerateJsRequest) => Promise<GenerateJsResponse>
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export const buildAIEndpoints = (API: BaseAPIClient): AIEndpoints => ({
|
|
@@ -14,4 +16,11 @@ export const buildAIEndpoints = (API: BaseAPIClient): AIEndpoints => ({
|
|
|
14
16
|
body: { prompt },
|
|
15
17
|
})
|
|
16
18
|
},
|
|
19
|
+
|
|
20
|
+
generateJs: async req => {
|
|
21
|
+
return await API.post({
|
|
22
|
+
url: "/api/ai/js",
|
|
23
|
+
body: req,
|
|
24
|
+
})
|
|
25
|
+
},
|
|
17
26
|
})
|
package/src/api/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { buildAuditLogEndpoints } from "./auditLogs"
|
|
|
45
45
|
import { buildLogsEndpoints } from "./logs"
|
|
46
46
|
import { buildMigrationEndpoints } from "./migrations"
|
|
47
47
|
import { buildRowActionEndpoints } from "./rowActions"
|
|
48
|
+
import { buildOAuth2Endpoints } from "./oauth2"
|
|
48
49
|
|
|
49
50
|
export type { APIClient } from "./types"
|
|
50
51
|
|
|
@@ -290,5 +291,6 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|
|
290
291
|
...buildMigrationEndpoints(API),
|
|
291
292
|
viewV2: buildViewV2Endpoints(API),
|
|
292
293
|
rowActions: buildRowActionEndpoints(API),
|
|
294
|
+
oauth2: buildOAuth2Endpoints(API),
|
|
293
295
|
}
|
|
294
296
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FetchOAuth2ConfigsResponse,
|
|
3
|
+
InsertOAuth2ConfigRequest,
|
|
4
|
+
InsertOAuth2ConfigResponse,
|
|
5
|
+
OAuth2ConfigResponse,
|
|
6
|
+
UpdateOAuth2ConfigRequest,
|
|
7
|
+
UpdateOAuth2ConfigResponse,
|
|
8
|
+
ValidateConfigRequest,
|
|
9
|
+
ValidateConfigResponse,
|
|
10
|
+
} from "@budibase/types"
|
|
11
|
+
import { BaseAPIClient } from "./types"
|
|
12
|
+
|
|
13
|
+
export interface OAuth2Endpoints {
|
|
14
|
+
fetch: () => Promise<OAuth2ConfigResponse[]>
|
|
15
|
+
create: (
|
|
16
|
+
config: InsertOAuth2ConfigRequest
|
|
17
|
+
) => Promise<InsertOAuth2ConfigResponse>
|
|
18
|
+
update: (
|
|
19
|
+
config: UpdateOAuth2ConfigRequest
|
|
20
|
+
) => Promise<UpdateOAuth2ConfigResponse>
|
|
21
|
+
delete: (id: string, rev: string) => Promise<void>
|
|
22
|
+
validate: (config: ValidateConfigRequest) => Promise<ValidateConfigResponse>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const buildOAuth2Endpoints = (API: BaseAPIClient): OAuth2Endpoints => ({
|
|
26
|
+
/**
|
|
27
|
+
* Gets all OAuth2 configurations for the app.
|
|
28
|
+
*/
|
|
29
|
+
fetch: async () => {
|
|
30
|
+
return (
|
|
31
|
+
await API.get<FetchOAuth2ConfigsResponse>({
|
|
32
|
+
url: `/api/oauth2`,
|
|
33
|
+
})
|
|
34
|
+
).configs
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a OAuth2 configuration.
|
|
39
|
+
*/
|
|
40
|
+
create: async config => {
|
|
41
|
+
return await API.post<
|
|
42
|
+
InsertOAuth2ConfigRequest,
|
|
43
|
+
InsertOAuth2ConfigResponse
|
|
44
|
+
>({
|
|
45
|
+
url: `/api/oauth2`,
|
|
46
|
+
body: {
|
|
47
|
+
...config,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Updates an existing OAuth2 configuration.
|
|
54
|
+
*/
|
|
55
|
+
update: async config => {
|
|
56
|
+
return await API.put<UpdateOAuth2ConfigRequest, UpdateOAuth2ConfigResponse>(
|
|
57
|
+
{
|
|
58
|
+
url: `/api/oauth2/${config._id}`,
|
|
59
|
+
body: {
|
|
60
|
+
...config,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Deletes an OAuth2 configuration by its id.
|
|
68
|
+
* @param id the ID of the OAuth2 config
|
|
69
|
+
* @param rev the rev of the OAuth2 config
|
|
70
|
+
*/
|
|
71
|
+
delete: async (id, rev) => {
|
|
72
|
+
return await API.delete<void, void>({
|
|
73
|
+
url: `/api/oauth2/${id}/${rev}`,
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
validate: async function (
|
|
77
|
+
config: ValidateConfigRequest
|
|
78
|
+
): Promise<ValidateConfigResponse> {
|
|
79
|
+
return await API.post<ValidateConfigRequest, ValidateConfigResponse>({
|
|
80
|
+
url: `/api/oauth2/validate`,
|
|
81
|
+
body: {
|
|
82
|
+
...config,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
})
|
package/src/api/types.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { LayoutEndpoints } from "./layouts"
|
|
|
16
16
|
import { LicensingEndpoints } from "./licensing"
|
|
17
17
|
import { LogEndpoints } from "./logs"
|
|
18
18
|
import { MigrationEndpoints } from "./migrations"
|
|
19
|
+
import { OAuth2Endpoints } from "./oauth2"
|
|
19
20
|
import { OtherEndpoints } from "./other"
|
|
20
21
|
import { PermissionEndpoints } from "./permissions"
|
|
21
22
|
import { PluginEndpoins } from "./plugins"
|
|
@@ -132,4 +133,8 @@ export type APIClient = BaseAPIClient &
|
|
|
132
133
|
TableEndpoints &
|
|
133
134
|
TemplateEndpoints &
|
|
134
135
|
UserEndpoints &
|
|
135
|
-
ViewEndpoints & {
|
|
136
|
+
ViewEndpoints & {
|
|
137
|
+
rowActions: RowActionEndpoints
|
|
138
|
+
viewV2: ViewV2Endpoints
|
|
139
|
+
oauth2: OAuth2Endpoints
|
|
140
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ModalContent, Body, notifications } from "@budibase/bbui"
|
|
3
|
+
import PasswordRepeatInput from "./PasswordRepeatInput.svelte"
|
|
4
|
+
import type { APIClient } from "@budibase/frontend-core"
|
|
5
|
+
import { createEventDispatcher } from "svelte"
|
|
6
|
+
|
|
7
|
+
export let API: APIClient
|
|
8
|
+
export let passwordMinLength: string | undefined = undefined
|
|
9
|
+
export let notifySuccess = notifications.success
|
|
10
|
+
export let notifyError = notifications.error
|
|
11
|
+
|
|
12
|
+
const dispatch = createEventDispatcher()
|
|
13
|
+
|
|
14
|
+
let password: string = ""
|
|
15
|
+
let error: string = ""
|
|
16
|
+
|
|
17
|
+
const updatePassword = async () => {
|
|
18
|
+
try {
|
|
19
|
+
await API.updateSelf({ password })
|
|
20
|
+
notifySuccess("Password changed successfully")
|
|
21
|
+
dispatch("save")
|
|
22
|
+
} catch (error) {
|
|
23
|
+
notifyError("Failed to update password")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const handleKeydown = (evt: KeyboardEvent) => {
|
|
28
|
+
if (evt.key === "Enter" && !error && password) {
|
|
29
|
+
updatePassword()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<svelte:window on:keydown={handleKeydown} />
|
|
35
|
+
<ModalContent
|
|
36
|
+
title="Update password"
|
|
37
|
+
confirmText="Update password"
|
|
38
|
+
onConfirm={updatePassword}
|
|
39
|
+
disabled={!!error || !password}
|
|
40
|
+
>
|
|
41
|
+
<Body size="S">Enter your new password below.</Body>
|
|
42
|
+
<PasswordRepeatInput bind:password bind:error minLength={passwordMinLength} />
|
|
43
|
+
</ModalContent>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { FancyForm, FancyInput } from "@budibase/bbui"
|
|
3
|
+
import { createValidationStore, requiredValidator } from "../utils/validation"
|
|
4
|
+
|
|
5
|
+
export let passwordForm: FancyForm | undefined = undefined
|
|
6
|
+
export let password: string
|
|
7
|
+
export let error: string
|
|
8
|
+
export let minLength = "12"
|
|
9
|
+
|
|
10
|
+
const validatePassword = (value: string | undefined) => {
|
|
11
|
+
if (!value || value.length < parseInt(minLength)) {
|
|
12
|
+
return `Please enter at least ${minLength} characters. We recommend using machine generated or random passwords.`
|
|
13
|
+
}
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const [firstPassword, passwordError, firstTouched] = createValidationStore(
|
|
18
|
+
"",
|
|
19
|
+
requiredValidator
|
|
20
|
+
)
|
|
21
|
+
const [repeatPassword, _, repeatTouched] = createValidationStore(
|
|
22
|
+
"",
|
|
23
|
+
requiredValidator,
|
|
24
|
+
validatePassword
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
$: password = $firstPassword
|
|
28
|
+
$: firstPasswordError =
|
|
29
|
+
($firstTouched && $passwordError) ||
|
|
30
|
+
($repeatTouched && validatePassword(password))
|
|
31
|
+
$: error =
|
|
32
|
+
!$firstPassword ||
|
|
33
|
+
!$firstTouched ||
|
|
34
|
+
!$repeatTouched ||
|
|
35
|
+
$firstPassword !== $repeatPassword ||
|
|
36
|
+
firstPasswordError
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<FancyForm bind:this={passwordForm}>
|
|
40
|
+
<FancyInput
|
|
41
|
+
label="Password"
|
|
42
|
+
type="password"
|
|
43
|
+
error={firstPasswordError}
|
|
44
|
+
bind:value={$firstPassword}
|
|
45
|
+
/>
|
|
46
|
+
<FancyInput
|
|
47
|
+
label="Repeat password"
|
|
48
|
+
type="password"
|
|
49
|
+
error={$repeatTouched &&
|
|
50
|
+
$firstPassword !== $repeatPassword &&
|
|
51
|
+
"Passwords must match"}
|
|
52
|
+
bind:value={$repeatPassword}
|
|
53
|
+
/>
|
|
54
|
+
</FancyForm>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { writable } from "svelte/store"
|
|
3
|
+
import { ModalContent, Body, Input, notifications } from "@budibase/bbui"
|
|
4
|
+
import type { User, ContextUser } from "@budibase/types"
|
|
5
|
+
import type { APIClient } from "@budibase/frontend-core"
|
|
6
|
+
import { createEventDispatcher } from "svelte"
|
|
7
|
+
|
|
8
|
+
export let user: User | ContextUser | undefined = undefined
|
|
9
|
+
export let API: APIClient
|
|
10
|
+
export let notifySuccess = notifications.success
|
|
11
|
+
export let notifyError = notifications.error
|
|
12
|
+
|
|
13
|
+
const dispatch = createEventDispatcher()
|
|
14
|
+
|
|
15
|
+
const values = writable({
|
|
16
|
+
firstName: user?.firstName,
|
|
17
|
+
lastName: user?.lastName,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const updateInfo = async () => {
|
|
21
|
+
try {
|
|
22
|
+
await API.updateSelf($values)
|
|
23
|
+
notifySuccess("Information updated successfully")
|
|
24
|
+
dispatch("save")
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(error)
|
|
27
|
+
notifyError("Failed to update information")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<ModalContent title="My profile" confirmText="Save" onConfirm={updateInfo}>
|
|
33
|
+
<Body size="S">
|
|
34
|
+
Personalise the platform by adding your first name and last name.
|
|
35
|
+
</Body>
|
|
36
|
+
<Input disabled value={user?.email || ""} label="Email" />
|
|
37
|
+
<Input bind:value={$values.firstName} label="First name" />
|
|
38
|
+
<Input bind:value={$values.lastName} label="Last name" />
|
|
39
|
+
</ModalContent>
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
rowIdx={row?.__idx}
|
|
57
57
|
metadata={row?.__metadata?.row}
|
|
58
58
|
>
|
|
59
|
-
<div class="gutter">
|
|
59
|
+
<div class="gutter" class:selectable={$config.canSelectRows}>
|
|
60
60
|
{#if $$slots.default}
|
|
61
61
|
<slot />
|
|
62
62
|
{:else}
|
|
@@ -116,12 +116,9 @@
|
|
|
116
116
|
margin: 3px 0 0 0;
|
|
117
117
|
}
|
|
118
118
|
.number {
|
|
119
|
-
color:
|
|
120
|
-
}
|
|
121
|
-
.checkbox.visible,
|
|
122
|
-
.number.visible {
|
|
123
|
-
display: flex;
|
|
119
|
+
color: var(--spectrum-global-color-gray-500);
|
|
124
120
|
}
|
|
121
|
+
|
|
125
122
|
.delete,
|
|
126
123
|
.expand {
|
|
127
124
|
margin-right: 4px;
|
|
@@ -137,4 +134,11 @@
|
|
|
137
134
|
.delete:hover :global(.spectrum-Icon) {
|
|
138
135
|
color: var(--spectrum-global-color-red-600) !important;
|
|
139
136
|
}
|
|
137
|
+
|
|
138
|
+
/* Visibility of checkbox and number */
|
|
139
|
+
.gutter.selectable .checkbox.visible,
|
|
140
|
+
.number.visible,
|
|
141
|
+
.gutter:not(.selectable) .number {
|
|
142
|
+
display: flex;
|
|
143
|
+
}
|
|
140
144
|
</style>
|
|
@@ -303,9 +303,11 @@
|
|
|
303
303
|
/>
|
|
304
304
|
{/if}
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
<
|
|
308
|
-
|
|
306
|
+
{#if !$config.quiet}
|
|
307
|
+
<div class="column-icon">
|
|
308
|
+
<Icon size="S" name={getColumnIcon(column)} />
|
|
309
|
+
</div>
|
|
310
|
+
{/if}
|
|
309
311
|
<div class="search-icon" on:click={startSearching}>
|
|
310
312
|
<Icon hoverable size="S" name="Search" />
|
|
311
313
|
</div>
|
|
@@ -431,7 +433,7 @@
|
|
|
431
433
|
.header-cell :global(.cell) {
|
|
432
434
|
padding: 0 var(--cell-padding);
|
|
433
435
|
gap: calc(2 * var(--cell-spacing));
|
|
434
|
-
background: var(--
|
|
436
|
+
background: var(--header-cell-background);
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
/* Icon colors */
|
|
@@ -463,6 +465,7 @@
|
|
|
463
465
|
white-space: nowrap;
|
|
464
466
|
text-overflow: ellipsis;
|
|
465
467
|
overflow: hidden;
|
|
468
|
+
font-weight: 600;
|
|
466
469
|
}
|
|
467
470
|
.header-cell.searching .name {
|
|
468
471
|
opacity: 0;
|
|
@@ -217,6 +217,10 @@
|
|
|
217
217
|
--accent-color: var(--primaryColor, var(--spectrum-global-color-blue-400));
|
|
218
218
|
--grid-background: var(--spectrum-global-color-gray-50);
|
|
219
219
|
--grid-background-alt: var(--spectrum-global-color-gray-100);
|
|
220
|
+
--header-cell-background: var(
|
|
221
|
+
--custom-header-cell-background,
|
|
222
|
+
var(--spectrum-global-color-gray-100)
|
|
223
|
+
);
|
|
220
224
|
--cell-background: var(--grid-background);
|
|
221
225
|
--cell-background-hover: var(--grid-background-alt);
|
|
222
226
|
--cell-background-alt: var(--cell-background);
|
|
@@ -246,7 +250,10 @@
|
|
|
246
250
|
cursor: grabbing !important;
|
|
247
251
|
}
|
|
248
252
|
.grid.stripe {
|
|
249
|
-
--cell-background-alt: var(
|
|
253
|
+
--cell-background-alt: var(
|
|
254
|
+
--custom-stripe-cell-background,
|
|
255
|
+
var(--spectrum-global-color-gray-75)
|
|
256
|
+
);
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
/* Data layers */
|
|
@@ -352,11 +359,18 @@
|
|
|
352
359
|
|
|
353
360
|
/* Overrides for quiet */
|
|
354
361
|
.grid.quiet :global(.grid-data-content .row > .cell:not(:last-child)),
|
|
355
|
-
.grid.quiet :global(.sticky-column .row
|
|
356
|
-
.grid.quiet :global(.new-row .row > .cell:not(:last-child))
|
|
362
|
+
.grid.quiet :global(.sticky-column .row .cell),
|
|
363
|
+
.grid.quiet :global(.new-row .row > .cell:not(:last-child)),
|
|
364
|
+
.grid.quiet :global(.header-cell:not(:last-child) .cell) {
|
|
357
365
|
border-right: none;
|
|
358
366
|
}
|
|
359
367
|
.grid.quiet :global(.sticky-column:before) {
|
|
360
368
|
display: none;
|
|
361
369
|
}
|
|
370
|
+
.grid.quiet:not(.stripe) {
|
|
371
|
+
--header-cell-background: var(
|
|
372
|
+
--custom-header-cell-background,
|
|
373
|
+
var(--grid-background)
|
|
374
|
+
);
|
|
375
|
+
}
|
|
362
376
|
</style>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
|
5
5
|
import DataCell from "../cells/DataCell.svelte"
|
|
6
6
|
import { fade } from "svelte/transition"
|
|
7
|
-
import { GutterWidth, NewRowID } from "../lib/constants"
|
|
7
|
+
import { DefaultRowHeight, GutterWidth, NewRowID } from "../lib/constants"
|
|
8
8
|
import GutterCell from "../cells/GutterCell.svelte"
|
|
9
9
|
import KeyboardShortcut from "./KeyboardShortcut.svelte"
|
|
10
10
|
import { getCellID } from "../lib/utils"
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
columnRenderMap,
|
|
34
34
|
visibleColumns,
|
|
35
35
|
scrollTop,
|
|
36
|
+
height,
|
|
36
37
|
} = getContext("grid")
|
|
37
38
|
|
|
38
39
|
let visible = false
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
$: hasNoRows = !$rows.length
|
|
48
49
|
$: renderedRowCount = $renderedRows.length
|
|
49
50
|
$: offset = getOffset($hasNextPage, renderedRowCount, $rowHeight, $scrollTop)
|
|
51
|
+
$: spaceBelow = $height - offset - $rowHeight
|
|
52
|
+
$: flipButtons = spaceBelow < 36 + DefaultRowHeight
|
|
50
53
|
|
|
51
54
|
const getOffset = (hasNextPage, rowCount, rowHeight, scrollTop) => {
|
|
52
55
|
// If we have a next page of data then we aren't truly at the bottom, so we
|
|
@@ -244,7 +247,11 @@
|
|
|
244
247
|
</div>
|
|
245
248
|
</GridScrollWrapper>
|
|
246
249
|
</div>
|
|
247
|
-
<div
|
|
250
|
+
<div
|
|
251
|
+
class="buttons"
|
|
252
|
+
class:flip={flipButtons}
|
|
253
|
+
transition:fade|local={{ duration: 130 }}
|
|
254
|
+
>
|
|
248
255
|
<Button size="M" cta on:click={addRow} disabled={isAdding}>
|
|
249
256
|
<div class="button-with-keys">
|
|
250
257
|
Save
|
|
@@ -337,6 +344,9 @@
|
|
|
337
344
|
.button-with-keys :global(> div) {
|
|
338
345
|
padding-top: 2px;
|
|
339
346
|
}
|
|
347
|
+
.buttons.flip {
|
|
348
|
+
top: calc(var(--offset) - 36px - var(--default-row-height) / 2);
|
|
349
|
+
}
|
|
340
350
|
|
|
341
351
|
/* Sticky column styles */
|
|
342
352
|
.sticky-column {
|
|
@@ -2,8 +2,8 @@ export const SmallRowHeight = 36
|
|
|
2
2
|
export const MediumRowHeight = 64
|
|
3
3
|
export const LargeRowHeight = 92
|
|
4
4
|
export const DefaultRowHeight = SmallRowHeight
|
|
5
|
-
export const VPadding =
|
|
6
|
-
export const HPadding =
|
|
5
|
+
export const VPadding = 0
|
|
6
|
+
export const HPadding = 80
|
|
7
7
|
export const ScrollBarSize = 8
|
|
8
8
|
export const GutterWidth = 72
|
|
9
9
|
export const DefaultColumnWidth = 200
|
|
@@ -140,13 +140,13 @@
|
|
|
140
140
|
div {
|
|
141
141
|
position: absolute;
|
|
142
142
|
background: var(--spectrum-global-color-gray-500);
|
|
143
|
-
opacity: 0.
|
|
143
|
+
opacity: 0.35;
|
|
144
144
|
border-radius: 4px;
|
|
145
145
|
transition: opacity 130ms ease-out;
|
|
146
146
|
}
|
|
147
147
|
div:hover,
|
|
148
148
|
div.dragging {
|
|
149
|
-
opacity:
|
|
149
|
+
opacity: 0.8;
|
|
150
150
|
}
|
|
151
151
|
.v-scrollbar {
|
|
152
152
|
width: var(--scroll-bar-size);
|
|
@@ -7,8 +7,12 @@ type ConfigStore = {
|
|
|
7
7
|
[key in keyof BaseStoreProps]: Readable<BaseStoreProps[key]>
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface ConfigState extends BaseStoreProps {
|
|
11
|
+
canSelectRows: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
interface ConfigDerivedStore {
|
|
11
|
-
config: Readable<
|
|
15
|
+
config: Readable<ConfigState>
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
export type Store = ConfigStore & ConfigDerivedStore
|
|
@@ -47,7 +51,7 @@ export const deriveStores = (context: StoreContext): ConfigDerivedStore => {
|
|
|
47
51
|
const config = derived(
|
|
48
52
|
[props, definition, hasNonAutoColumn],
|
|
49
53
|
([$props, $definition, $hasNonAutoColumn]) => {
|
|
50
|
-
let config = { ...$props }
|
|
54
|
+
let config: ConfigState = { ...$props, canSelectRows: false }
|
|
51
55
|
const type = $props.datasource?.type
|
|
52
56
|
|
|
53
57
|
// Disable some features if we're editing a view
|
|
@@ -78,6 +82,9 @@ export const deriveStores = (context: StoreContext): ConfigDerivedStore => {
|
|
|
78
82
|
config.canEditColumns = false
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
// Determine if we can select rows
|
|
86
|
+
config.canSelectRows = !!config.canDeleteRows || !!config.canAddRows
|
|
87
|
+
|
|
81
88
|
return config
|
|
82
89
|
}
|
|
83
90
|
)
|
|
@@ -36,6 +36,7 @@ const DependencyOrderedStores = [
|
|
|
36
36
|
NonPlus,
|
|
37
37
|
Datasource,
|
|
38
38
|
Columns,
|
|
39
|
+
Config as any,
|
|
39
40
|
Scroll,
|
|
40
41
|
Validation,
|
|
41
42
|
Rows,
|
|
@@ -47,7 +48,6 @@ const DependencyOrderedStores = [
|
|
|
47
48
|
Users,
|
|
48
49
|
Menu,
|
|
49
50
|
Pagination,
|
|
50
|
-
Config as any,
|
|
51
51
|
Clipboard,
|
|
52
52
|
Notifications,
|
|
53
53
|
Cache,
|
|
@@ -58,6 +58,7 @@ export const deriveStores = (context: StoreContext) => {
|
|
|
58
58
|
width,
|
|
59
59
|
height,
|
|
60
60
|
buttonColumnWidth,
|
|
61
|
+
config,
|
|
61
62
|
} = context
|
|
62
63
|
|
|
63
64
|
// Memoize store primitives
|
|
@@ -97,11 +98,14 @@ export const deriveStores = (context: StoreContext) => {
|
|
|
97
98
|
|
|
98
99
|
// Derive vertical limits
|
|
99
100
|
const contentHeight = derived(
|
|
100
|
-
[rows, rowHeight, showHScrollbar],
|
|
101
|
-
([$rows, $rowHeight, $showHScrollbar]) => {
|
|
102
|
-
let height =
|
|
101
|
+
[rows, rowHeight, showHScrollbar, config],
|
|
102
|
+
([$rows, $rowHeight, $showHScrollbar, $config]) => {
|
|
103
|
+
let height = $rows.length * $rowHeight + VPadding
|
|
103
104
|
if ($showHScrollbar) {
|
|
104
|
-
height += ScrollBarSize *
|
|
105
|
+
height += ScrollBarSize * 3
|
|
106
|
+
}
|
|
107
|
+
if ($config.canAddRows) {
|
|
108
|
+
height += $rowHeight
|
|
105
109
|
}
|
|
106
110
|
return height
|
|
107
111
|
}
|
package/src/components/index.js
CHANGED
|
@@ -9,3 +9,6 @@ export { Grid } from "./grid"
|
|
|
9
9
|
export { default as ClientAppSkeleton } from "./ClientAppSkeleton.svelte"
|
|
10
10
|
export { default as CoreFilterBuilder } from "./CoreFilterBuilder.svelte"
|
|
11
11
|
export { default as FilterUsers } from "./FilterUsers.svelte"
|
|
12
|
+
export { default as ChangePasswordModal } from "./ChangePasswordModal.svelte"
|
|
13
|
+
export { default as ProfileModal } from "./ProfileModal.svelte"
|
|
14
|
+
export { default as PasswordRepeatInput } from "./PasswordRepeatInput.svelte"
|
package/src/constants.ts
CHANGED
|
@@ -166,3 +166,9 @@ export const FieldPermissions = {
|
|
|
166
166
|
READONLY: "readonly",
|
|
167
167
|
HIDDEN: "hidden",
|
|
168
168
|
}
|
|
169
|
+
|
|
170
|
+
// one or more word characters and whitespace
|
|
171
|
+
export const APP_NAME_REGEX = /^[\w\s]+$/
|
|
172
|
+
|
|
173
|
+
// zero or more non-whitespace characters
|
|
174
|
+
export const APP_URL_REGEX = /^[0-9a-zA-Z-_]+$/
|
package/src/fetch/DataFetch.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Row,
|
|
9
9
|
SearchFilters,
|
|
10
10
|
SortOrder,
|
|
11
|
+
SortType,
|
|
11
12
|
TableSchema,
|
|
12
13
|
} from "@budibase/types"
|
|
13
14
|
import { APIClient } from "../api/types"
|
|
@@ -71,6 +72,8 @@ export default abstract class BaseDataFetch<
|
|
|
71
72
|
options: DataFetchOptions<TQuery> & {
|
|
72
73
|
datasource: TDatasource
|
|
73
74
|
|
|
75
|
+
sortType: SortType | null
|
|
76
|
+
|
|
74
77
|
// Client side feature customisation
|
|
75
78
|
clientSideSearching: boolean
|
|
76
79
|
clientSideSorting: boolean
|
|
@@ -103,6 +106,7 @@ export default abstract class BaseDataFetch<
|
|
|
103
106
|
// Sorting config
|
|
104
107
|
sortColumn: null,
|
|
105
108
|
sortOrder: SortOrder.ASCENDING,
|
|
109
|
+
sortType: null,
|
|
106
110
|
|
|
107
111
|
// Pagination config
|
|
108
112
|
paginate: true,
|
|
@@ -223,12 +227,31 @@ export default abstract class BaseDataFetch<
|
|
|
223
227
|
this.options.sortColumn = this.getDefaultSortColumn(definition, schema)
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
// If
|
|
227
|
-
|
|
230
|
+
// If we don't have a sort column specified then just ensure we don't set
|
|
231
|
+
// any sorting params
|
|
232
|
+
if (!this.options.sortColumn) {
|
|
228
233
|
this.options.sortOrder = SortOrder.ASCENDING
|
|
234
|
+
this.options.sortType = null
|
|
229
235
|
} else {
|
|
230
|
-
//
|
|
231
|
-
this.options.
|
|
236
|
+
// Otherwise determine what sort type to use base on sort column
|
|
237
|
+
this.options.sortType = SortType.STRING
|
|
238
|
+
const fieldSchema = schema?.[this.options.sortColumn]
|
|
239
|
+
if (
|
|
240
|
+
fieldSchema?.type === FieldType.NUMBER ||
|
|
241
|
+
fieldSchema?.type === FieldType.BIGINT ||
|
|
242
|
+
("calculationType" in fieldSchema && fieldSchema?.calculationType)
|
|
243
|
+
) {
|
|
244
|
+
this.options.sortType = SortType.NUMBER
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// If no sort order, default to ascending
|
|
248
|
+
if (!this.options.sortOrder) {
|
|
249
|
+
this.options.sortOrder = SortOrder.ASCENDING
|
|
250
|
+
} else {
|
|
251
|
+
// Ensure sortOrder matches the enum
|
|
252
|
+
this.options.sortOrder =
|
|
253
|
+
this.options.sortOrder.toLowerCase() as SortOrder
|
|
254
|
+
}
|
|
232
255
|
}
|
|
233
256
|
|
|
234
257
|
// Build the query
|
|
@@ -271,6 +294,7 @@ export default abstract class BaseDataFetch<
|
|
|
271
294
|
const {
|
|
272
295
|
sortColumn,
|
|
273
296
|
sortOrder,
|
|
297
|
+
sortType,
|
|
274
298
|
limit,
|
|
275
299
|
clientSideSearching,
|
|
276
300
|
clientSideSorting,
|
|
@@ -287,8 +311,8 @@ export default abstract class BaseDataFetch<
|
|
|
287
311
|
}
|
|
288
312
|
|
|
289
313
|
// If we don't support sorting, do a client-side sort
|
|
290
|
-
if (!this.features.supportsSort && clientSideSorting &&
|
|
291
|
-
rows = sort(rows, sortColumn, sortOrder)
|
|
314
|
+
if (!this.features.supportsSort && clientSideSorting && sortType) {
|
|
315
|
+
rows = sort(rows, sortColumn as any, sortOrder, sortType)
|
|
292
316
|
}
|
|
293
317
|
|
|
294
318
|
// If we don't support pagination, do a client-side limit
|
package/src/fetch/TableFetch.ts
CHANGED
|
@@ -29,7 +29,8 @@ export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async getData() {
|
|
32
|
-
const { datasource, limit, sortColumn, sortOrder, paginate } =
|
|
32
|
+
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
|
|
33
|
+
this.options
|
|
33
34
|
const { tableId } = datasource
|
|
34
35
|
const { cursor, query } = get(this.store)
|
|
35
36
|
|
|
@@ -40,6 +41,7 @@ export default class TableFetch extends BaseDataFetch<TableDatasource, Table> {
|
|
|
40
41
|
limit,
|
|
41
42
|
sort: sortColumn,
|
|
42
43
|
sortOrder: sortOrder ?? SortOrder.ASCENDING,
|
|
44
|
+
sortType,
|
|
43
45
|
paginate,
|
|
44
46
|
bookmark: cursor,
|
|
45
47
|
})
|
package/src/fetch/ViewV2Fetch.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
SearchViewRowRequest,
|
|
3
2
|
SortOrder,
|
|
4
3
|
ViewDatasource,
|
|
5
4
|
ViewV2Enriched,
|
|
@@ -41,7 +40,8 @@ export default class ViewV2Fetch extends BaseDataFetch<
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
async getData() {
|
|
44
|
-
const { datasource, limit, sortColumn, sortOrder, paginate } =
|
|
43
|
+
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
|
|
44
|
+
this.options
|
|
45
45
|
const { cursor, query, definition } = get(this.store)
|
|
46
46
|
|
|
47
47
|
// If this is a calculation view and we have no calculations, return nothing
|
|
@@ -68,13 +68,14 @@ export default class ViewV2Fetch extends BaseDataFetch<
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
try {
|
|
71
|
-
const request
|
|
71
|
+
const request = {
|
|
72
72
|
query,
|
|
73
73
|
paginate,
|
|
74
74
|
limit,
|
|
75
75
|
bookmark: cursor,
|
|
76
76
|
sort: sortColumn,
|
|
77
77
|
sortOrder: sortOrder,
|
|
78
|
+
sortType,
|
|
78
79
|
}
|
|
79
80
|
if (paginate) {
|
|
80
81
|
const res = await this.API.viewV2.fetch(datasource.id, {
|
package/src/utils/index.ts
CHANGED
package/src/utils/schema.js
CHANGED
|
@@ -2,7 +2,9 @@ import { helpers } from "@budibase/shared-core"
|
|
|
2
2
|
import { TypeIconMap } from "../constants"
|
|
3
3
|
|
|
4
4
|
export const getColumnIcon = column => {
|
|
5
|
-
|
|
5
|
+
// For some reason we have remix icons saved under this property sometimes,
|
|
6
|
+
// so we must ignore those as they are invalid spectrum icons
|
|
7
|
+
if (column.schema.icon && !column.schema.icon.startsWith("ri-")) {
|
|
6
8
|
return column.schema.icon
|
|
7
9
|
}
|
|
8
10
|
if (column.calculationType) {
|
package/src/utils/utils.ts
CHANGED
|
@@ -397,14 +397,19 @@ export function parseFilter(filter: UISearchFilter) {
|
|
|
397
397
|
|
|
398
398
|
const update = cloneDeep(filter)
|
|
399
399
|
|
|
400
|
-
update.groups
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
if (update.groups) {
|
|
401
|
+
update.groups = update.groups
|
|
402
|
+
.map(group => {
|
|
403
|
+
if (group.filters) {
|
|
404
|
+
group.filters = group.filters.filter((filter: any) => {
|
|
405
|
+
return filter.field && filter.operator
|
|
406
|
+
})
|
|
407
|
+
return group.filters?.length ? group : null
|
|
408
|
+
}
|
|
409
|
+
return group
|
|
404
410
|
})
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.filter((group): group is SearchFilterGroup => !!group)
|
|
411
|
+
.filter((group): group is SearchFilterGroup => !!group)
|
|
412
|
+
}
|
|
408
413
|
|
|
409
414
|
return update
|
|
410
415
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { writable, derived } from "svelte/store"
|
|
2
|
+
|
|
3
|
+
// DEPRECATED - Use the yup based validators for future validation
|
|
4
|
+
|
|
5
|
+
export function createValidationStore(initialValue, ...validators) {
|
|
6
|
+
let touched = false
|
|
7
|
+
|
|
8
|
+
const value = writable(initialValue || "")
|
|
9
|
+
const touchedStore = derived(value, () => {
|
|
10
|
+
if (!touched) {
|
|
11
|
+
touched = true
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
return touched
|
|
15
|
+
})
|
|
16
|
+
const error = derived(
|
|
17
|
+
[value, touchedStore],
|
|
18
|
+
([$v, $t]) => $t && validate($v, validators)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return [value, error, touchedStore]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validate(value, validators) {
|
|
25
|
+
const failing = validators.find(v => v(value) !== true)
|
|
26
|
+
|
|
27
|
+
return failing && failing(value)
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// TODO: Convert to yup based validators
|
|
2
|
+
|
|
3
|
+
export function emailValidator(value) {
|
|
4
|
+
return (
|
|
5
|
+
(value &&
|
|
6
|
+
!!value.match(
|
|
7
|
+
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
|
8
|
+
)) ||
|
|
9
|
+
"Please enter a valid email"
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function requiredValidator(value) {
|
|
14
|
+
return (
|
|
15
|
+
(value !== undefined && value !== null && value !== "") ||
|
|
16
|
+
"This field is required"
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { string, mixed } from "yup"
|
|
2
|
+
import { APP_NAME_REGEX, APP_URL_REGEX } from "../../../constants"
|
|
3
|
+
|
|
4
|
+
export const name = (validation, { apps, currentApp } = { apps: [] }) => {
|
|
5
|
+
validation.addValidator(
|
|
6
|
+
"name",
|
|
7
|
+
string()
|
|
8
|
+
.trim()
|
|
9
|
+
.required("Your application must have a name")
|
|
10
|
+
.matches(
|
|
11
|
+
APP_NAME_REGEX,
|
|
12
|
+
"App name must be letters, numbers and spaces only"
|
|
13
|
+
)
|
|
14
|
+
.test(
|
|
15
|
+
"non-existing-app-name",
|
|
16
|
+
"Another app with the same name already exists",
|
|
17
|
+
value => {
|
|
18
|
+
if (!value) {
|
|
19
|
+
// exit early, above validator will fail
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
return !apps
|
|
23
|
+
.filter(app => {
|
|
24
|
+
return app.appId !== currentApp?.appId
|
|
25
|
+
})
|
|
26
|
+
.map(app => app.name)
|
|
27
|
+
.some(appName => appName.toLowerCase() === value.toLowerCase())
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const url = (validation, { apps, currentApp } = { apps: [] }) => {
|
|
34
|
+
validation.addValidator(
|
|
35
|
+
"url",
|
|
36
|
+
string()
|
|
37
|
+
.trim()
|
|
38
|
+
.nullable()
|
|
39
|
+
.required("Your application must have a url")
|
|
40
|
+
.matches(APP_URL_REGEX, "Please enter a valid url")
|
|
41
|
+
.test(
|
|
42
|
+
"non-existing-app-url",
|
|
43
|
+
"Another app with the same URL already exists",
|
|
44
|
+
value => {
|
|
45
|
+
if (!value) {
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
if (currentApp) {
|
|
49
|
+
// filter out the current app if present
|
|
50
|
+
apps = apps.filter(app => app.appId !== currentApp.appId)
|
|
51
|
+
}
|
|
52
|
+
return !apps
|
|
53
|
+
.map(app => app.url)
|
|
54
|
+
.some(appUrl => {
|
|
55
|
+
const url =
|
|
56
|
+
appUrl?.[0] === "/"
|
|
57
|
+
? appUrl.substring(1, appUrl.length)
|
|
58
|
+
: appUrl
|
|
59
|
+
return url?.toLowerCase() === value.toLowerCase()
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
.test("valid-url", "Not a valid URL", value => {
|
|
64
|
+
// url is nullable
|
|
65
|
+
if (!value) {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
// make it clear that this is a url path and cannot be a full url
|
|
69
|
+
return (
|
|
70
|
+
!value.includes("http") &&
|
|
71
|
+
!value.includes("www") &&
|
|
72
|
+
!value.includes(".")
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const file = (validation, { template } = {}) => {
|
|
79
|
+
const templateToUse =
|
|
80
|
+
template && Object.keys(template).length === 0 ? null : template
|
|
81
|
+
validation.addValidator(
|
|
82
|
+
"file",
|
|
83
|
+
templateToUse?.fromFile
|
|
84
|
+
? mixed().required("Please choose a file to import")
|
|
85
|
+
: null
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { object, string, number } from "yup"
|
|
2
|
+
import { writable, get } from "svelte/store"
|
|
3
|
+
import { Helpers, notifications } from "@budibase/bbui"
|
|
4
|
+
|
|
5
|
+
export const createValidationStore = () => {
|
|
6
|
+
const DEFAULT = {
|
|
7
|
+
values: {},
|
|
8
|
+
errors: {},
|
|
9
|
+
touched: {},
|
|
10
|
+
valid: false,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const validator = {}
|
|
14
|
+
const validation = writable(DEFAULT)
|
|
15
|
+
|
|
16
|
+
const addValidator = (propertyName, propertyValidator) => {
|
|
17
|
+
if (!propertyValidator || !propertyName) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
validator[propertyName] = propertyValidator
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const addValidatorType = (propertyName, type, required, options) => {
|
|
24
|
+
if (!type || !propertyName) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let propertyValidator
|
|
29
|
+
switch (type) {
|
|
30
|
+
case "number":
|
|
31
|
+
propertyValidator = number().nullable()
|
|
32
|
+
break
|
|
33
|
+
case "email":
|
|
34
|
+
propertyValidator = string().email().nullable()
|
|
35
|
+
break
|
|
36
|
+
case "password":
|
|
37
|
+
propertyValidator = string().nullable()
|
|
38
|
+
break
|
|
39
|
+
default:
|
|
40
|
+
propertyValidator = string().nullable()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (required) {
|
|
44
|
+
propertyValidator = propertyValidator.required()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options?.minLength) {
|
|
48
|
+
propertyValidator = propertyValidator.min(options.minLength)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
validator[propertyName] = propertyValidator
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const observe = async (propertyName, value) => {
|
|
55
|
+
const values = get(validation).values
|
|
56
|
+
let fieldIsValid
|
|
57
|
+
if (!Object.prototype.hasOwnProperty.call(values, propertyName)) {
|
|
58
|
+
// Initial setup
|
|
59
|
+
values[propertyName] = value
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (value === values[propertyName]) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const obj = object().shape(validator)
|
|
68
|
+
try {
|
|
69
|
+
validation.update(store => {
|
|
70
|
+
store.errors[propertyName] = null
|
|
71
|
+
return store
|
|
72
|
+
})
|
|
73
|
+
await obj.validateAt(propertyName, { [propertyName]: value })
|
|
74
|
+
fieldIsValid = true
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const [fieldError] = error.errors
|
|
77
|
+
if (fieldError) {
|
|
78
|
+
validation.update(store => {
|
|
79
|
+
store.errors[propertyName] = Helpers.capitalise(fieldError)
|
|
80
|
+
store.valid = false
|
|
81
|
+
return store
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (fieldIsValid) {
|
|
87
|
+
// Validate the rest of the fields
|
|
88
|
+
try {
|
|
89
|
+
await obj.validate(
|
|
90
|
+
{ ...values, [propertyName]: value },
|
|
91
|
+
{ abortEarly: false }
|
|
92
|
+
)
|
|
93
|
+
validation.update(store => {
|
|
94
|
+
store.valid = true
|
|
95
|
+
return store
|
|
96
|
+
})
|
|
97
|
+
} catch {
|
|
98
|
+
validation.update(store => {
|
|
99
|
+
store.valid = false
|
|
100
|
+
return store
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const check = async values => {
|
|
107
|
+
const obj = object().shape(validator)
|
|
108
|
+
// clear the previous errors
|
|
109
|
+
const properties = Object.keys(validator)
|
|
110
|
+
properties.forEach(property => (get(validation).errors[property] = null))
|
|
111
|
+
|
|
112
|
+
let validationError = false
|
|
113
|
+
try {
|
|
114
|
+
await obj.validate(values, { abortEarly: false })
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (!error.inner) {
|
|
117
|
+
notifications.error("Unexpected validation error", error)
|
|
118
|
+
validationError = true
|
|
119
|
+
} else {
|
|
120
|
+
error.inner.forEach(err => {
|
|
121
|
+
validation.update(store => {
|
|
122
|
+
store.errors[err.path] = Helpers.capitalise(err.message)
|
|
123
|
+
return store
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let valid
|
|
130
|
+
if (properties.length && !validationError) {
|
|
131
|
+
valid = await obj.isValid(values)
|
|
132
|
+
} else {
|
|
133
|
+
// don't say valid until validators have been loaded
|
|
134
|
+
valid = false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
validation.update(store => {
|
|
138
|
+
store.valid = valid
|
|
139
|
+
return store
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
subscribe: validation.subscribe,
|
|
145
|
+
set: validation.set,
|
|
146
|
+
check,
|
|
147
|
+
addValidator,
|
|
148
|
+
addValidatorType,
|
|
149
|
+
observe,
|
|
150
|
+
}
|
|
151
|
+
}
|