@equinor/apollo-utils 0.1.3-beta.3 → 0.1.3-beta.5

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.
@@ -0,0 +1,46 @@
1
+ import { PrimitiveAtom } from 'jotai';
2
+ import { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
3
+ import { z } from 'zod';
4
+
5
+ type ValidationErrorMap<T> = Map<keyof T, {
6
+ message: string;
7
+ code: string;
8
+ }>;
9
+
10
+ declare function createValidator<S extends z.ZodTypeAny>(schema: S): {
11
+ validate: <E extends z.TypeOf<S>>(entity: E) => ValidationErrorMap<E> | undefined;
12
+ validateAsync: <E_1 extends z.TypeOf<S>>(entity: z.infer<typeof schema>) => Promise<ValidationErrorMap<E_1> | undefined>;
13
+ getSchema(): S;
14
+ };
15
+
16
+ type FormState<T> = {
17
+ status: 'editing' | 'pending';
18
+ values: T;
19
+ errors?: ValidationErrorMap<T>;
20
+ isValid?: boolean;
21
+ };
22
+ type FormFamilyParam = {
23
+ id: number | string;
24
+ } & Record<string, unknown>;
25
+
26
+ declare function createFormFamily<E extends Record<string, unknown>>(): AtomFamily<FormFamilyParam, PrimitiveAtom<FormState<E> | undefined> & {
27
+ init: FormState<E> | undefined;
28
+ }>;
29
+ type FormFamily<T> = AtomFamily<FormFamilyParam, PrimitiveAtom<FormState<T> | undefined>>;
30
+ type FormValidator = ReturnType<typeof createValidator>;
31
+ type UseFormFamilyUtilsOptions = {
32
+ validator?: FormValidator;
33
+ };
34
+ declare function useFormFamilyUtils<T>(family: FormFamily<T>, options?: UseFormFamilyUtilsOptions): {
35
+ useFormStateAtom: (param: FormFamilyParam) => [FormState<T> | undefined, (args_0: FormState<T> | ((prev: FormState<T> | undefined) => FormState<T> | undefined) | undefined) => void];
36
+ useFormState: (param: FormFamilyParam) => FormState<T> | undefined;
37
+ useUpdateFormMutation: (param: FormFamilyParam) => (update: Partial<T>) => void;
38
+ useFormMutations(param: FormFamilyParam): {
39
+ update: (update: Partial<T>) => void;
40
+ initializeForm: (entity: T) => void;
41
+ resetForm: () => void;
42
+ };
43
+ };
44
+ declare function useFormFamilyMutation<T>(family: FormFamily<T>, param: FormFamilyParam, validator?: FormValidator): (update: Partial<T>) => void;
45
+
46
+ export { FormFamilyParam, FormState, ValidationErrorMap, createFormFamily, createValidator, useFormFamilyMutation, useFormFamilyUtils };
package/dist/index.js ADDED
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __spreadValues = (a, b) => {
10
+ for (var prop in b || (b = {}))
11
+ if (__hasOwnProp.call(b, prop))
12
+ __defNormalProp(a, prop, b[prop]);
13
+ if (__getOwnPropSymbols)
14
+ for (var prop of __getOwnPropSymbols(b)) {
15
+ if (__propIsEnum.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ }
18
+ return a;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, { get: all[name], enumerable: true });
23
+ };
24
+ var __copyProps = (to, from, except, desc) => {
25
+ if (from && typeof from === "object" || typeof from === "function") {
26
+ for (let key of __getOwnPropNames(from))
27
+ if (!__hasOwnProp.call(to, key) && key !== except)
28
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
29
+ }
30
+ return to;
31
+ };
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+ var __async = (__this, __arguments, generator) => {
34
+ return new Promise((resolve, reject) => {
35
+ var fulfilled = (value) => {
36
+ try {
37
+ step(generator.next(value));
38
+ } catch (e) {
39
+ reject(e);
40
+ }
41
+ };
42
+ var rejected = (value) => {
43
+ try {
44
+ step(generator.throw(value));
45
+ } catch (e) {
46
+ reject(e);
47
+ }
48
+ };
49
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
50
+ step((generator = generator.apply(__this, __arguments)).next());
51
+ });
52
+ };
53
+
54
+ // src/index.ts
55
+ var src_exports = {};
56
+ __export(src_exports, {
57
+ createFormFamily: () => createFormFamily,
58
+ createValidator: () => createValidator,
59
+ useFormFamilyMutation: () => useFormFamilyMutation,
60
+ useFormFamilyUtils: () => useFormFamilyUtils
61
+ });
62
+ module.exports = __toCommonJS(src_exports);
63
+
64
+ // src/jotai-form/formUtils.ts
65
+ var import_jotai = require("jotai");
66
+ var import_utils = require("jotai/utils");
67
+ function createFormFamily() {
68
+ return (0, import_utils.atomFamily)(
69
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
+ (_param) => (0, import_jotai.atom)(void 0),
71
+ (a, b) => a.id === b.id
72
+ );
73
+ }
74
+ function useFormFamilyUtils(family, options = {}) {
75
+ return {
76
+ useFormStateAtom: (param) => (0, import_jotai.useAtom)(family(param)),
77
+ useFormState: (param) => (0, import_jotai.useAtomValue)(family(param)),
78
+ useUpdateFormMutation: (param) => {
79
+ return useFormFamilyMutation(family, param, options.validator);
80
+ },
81
+ useFormMutations(param) {
82
+ const mutateAtom = (0, import_jotai.useSetAtom)(family(param));
83
+ return {
84
+ update: useFormFamilyMutation(family, param, options.validator),
85
+ initializeForm: (entity) => mutateAtom({
86
+ status: "editing",
87
+ values: entity,
88
+ isValid: true
89
+ }),
90
+ resetForm: () => mutateAtom(void 0)
91
+ };
92
+ }
93
+ };
94
+ }
95
+ function useFormFamilyMutation(family, param, validator) {
96
+ const mutate = (0, import_jotai.useSetAtom)(family(param));
97
+ return (update) => {
98
+ return mutate((previous) => {
99
+ if (!previous)
100
+ return;
101
+ const updatedValues = __spreadValues(__spreadValues({}, previous.values), update);
102
+ const errors = validator == null ? void 0 : validator.validate(updatedValues);
103
+ return {
104
+ status: "editing",
105
+ values: updatedValues,
106
+ errors,
107
+ isValid: !errors
108
+ };
109
+ });
110
+ };
111
+ }
112
+
113
+ // src/zod-validation/utils.ts
114
+ function createValidator(schema) {
115
+ return {
116
+ validate: (entity) => {
117
+ const validation = schema.safeParse(entity);
118
+ if (validation.success)
119
+ return void 0;
120
+ return prepareErrors(validation);
121
+ },
122
+ validateAsync: (entity) => __async(this, null, function* () {
123
+ const validation = yield schema.safeParseAsync(entity);
124
+ if (validation.success)
125
+ return void 0;
126
+ return prepareErrors(validation);
127
+ }),
128
+ getSchema() {
129
+ return schema;
130
+ }
131
+ };
132
+ }
133
+ function prepareErrors(errorValidation) {
134
+ return new Map(
135
+ errorValidation.error.errors.map((error) => [
136
+ error.path[0],
137
+ { message: error.message, code: error.code }
138
+ ])
139
+ );
140
+ }
141
+ // Annotate the CommonJS export names for ESM import in node:
142
+ 0 && (module.exports = {
143
+ createFormFamily,
144
+ createValidator,
145
+ useFormFamilyMutation,
146
+ useFormFamilyUtils
147
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,120 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
17
+ var __async = (__this, __arguments, generator) => {
18
+ return new Promise((resolve, reject) => {
19
+ var fulfilled = (value) => {
20
+ try {
21
+ step(generator.next(value));
22
+ } catch (e) {
23
+ reject(e);
24
+ }
25
+ };
26
+ var rejected = (value) => {
27
+ try {
28
+ step(generator.throw(value));
29
+ } catch (e) {
30
+ reject(e);
31
+ }
32
+ };
33
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
34
+ step((generator = generator.apply(__this, __arguments)).next());
35
+ });
36
+ };
37
+
38
+ // src/jotai-form/formUtils.ts
39
+ import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
40
+ import { atomFamily } from "jotai/utils";
41
+ function createFormFamily() {
42
+ return atomFamily(
43
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
44
+ (_param) => atom(void 0),
45
+ (a, b) => a.id === b.id
46
+ );
47
+ }
48
+ function useFormFamilyUtils(family, options = {}) {
49
+ return {
50
+ useFormStateAtom: (param) => useAtom(family(param)),
51
+ useFormState: (param) => useAtomValue(family(param)),
52
+ useUpdateFormMutation: (param) => {
53
+ return useFormFamilyMutation(family, param, options.validator);
54
+ },
55
+ useFormMutations(param) {
56
+ const mutateAtom = useSetAtom(family(param));
57
+ return {
58
+ update: useFormFamilyMutation(family, param, options.validator),
59
+ initializeForm: (entity) => mutateAtom({
60
+ status: "editing",
61
+ values: entity,
62
+ isValid: true
63
+ }),
64
+ resetForm: () => mutateAtom(void 0)
65
+ };
66
+ }
67
+ };
68
+ }
69
+ function useFormFamilyMutation(family, param, validator) {
70
+ const mutate = useSetAtom(family(param));
71
+ return (update) => {
72
+ return mutate((previous) => {
73
+ if (!previous)
74
+ return;
75
+ const updatedValues = __spreadValues(__spreadValues({}, previous.values), update);
76
+ const errors = validator == null ? void 0 : validator.validate(updatedValues);
77
+ return {
78
+ status: "editing",
79
+ values: updatedValues,
80
+ errors,
81
+ isValid: !errors
82
+ };
83
+ });
84
+ };
85
+ }
86
+
87
+ // src/zod-validation/utils.ts
88
+ function createValidator(schema) {
89
+ return {
90
+ validate: (entity) => {
91
+ const validation = schema.safeParse(entity);
92
+ if (validation.success)
93
+ return void 0;
94
+ return prepareErrors(validation);
95
+ },
96
+ validateAsync: (entity) => __async(this, null, function* () {
97
+ const validation = yield schema.safeParseAsync(entity);
98
+ if (validation.success)
99
+ return void 0;
100
+ return prepareErrors(validation);
101
+ }),
102
+ getSchema() {
103
+ return schema;
104
+ }
105
+ };
106
+ }
107
+ function prepareErrors(errorValidation) {
108
+ return new Map(
109
+ errorValidation.error.errors.map((error) => [
110
+ error.path[0],
111
+ { message: error.message, code: error.code }
112
+ ])
113
+ );
114
+ }
115
+ export {
116
+ createFormFamily,
117
+ createValidator,
118
+ useFormFamilyMutation,
119
+ useFormFamilyUtils
120
+ };
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@equinor/apollo-utils",
3
- "main": "src/index.ts",
4
- "types": "src/index.ts",
5
- "version": "0.1.3-beta.3",
3
+ "version": "0.1.3-beta.5",
6
4
  "license": "MIT",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist/**"
10
+ ],
7
11
  "scripts": {
8
12
  "build": "tsup src/index.ts --format esm,cjs --dts --external react",
9
13
  "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
@@ -13,21 +17,16 @@
13
17
  "test": "vitest --run",
14
18
  "test:watch": "vitest --watch"
15
19
  },
16
- "publishConfig": {
17
- "access": "public"
18
- },
19
20
  "dependencies": {
20
21
  "jotai": "^2.0.1",
21
22
  "tsup": "^6.6.3",
22
- "typescript": "^4.9.5",
23
23
  "zod": "^3.20.6"
24
24
  },
25
25
  "peerDependencies": {
26
- "react": "^18.2.0",
27
- "react-dom": "^18.2.0",
28
- "react-hook-form": "^7.43.8",
29
26
  "@equinor/eds-core-react": "^0.27.0",
30
- "styled-components": "^5.3.7"
27
+ "styled-components": "^5.3.7",
28
+ "react": "^18.2.0",
29
+ "react-dom": "^18.2.0"
31
30
  },
32
31
  "devDependencies": {
33
32
  "@testing-library/react": "^13.4.0",
@@ -35,10 +34,15 @@
35
34
  "@vitest/ui": "^0.28.5",
36
35
  "eslint": "^8.34.0",
37
36
  "eslint-config-custom": "*",
37
+ "@types/react": "^18.0.1",
38
+ "@types/react-dom": "^18.0.1",
39
+ "@types/styled-components": "^5.1.26",
38
40
  "jsdom": "^21.1.0",
39
- "react": "^18.2.0",
40
- "react-dom": "^18.2.0",
41
- "react-hook-form": "^7.43.8",
42
- "vitest": "^0.28.5"
41
+ "vitest": "^0.28.5",
42
+ "tsconfig": "*",
43
+ "typescript": "^4.9.5"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
43
47
  }
44
48
  }
package/.eslintrc.cjs DELETED
@@ -1,4 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- extends: ['custom'],
4
- }
package/CHANGELOG.md DELETED
@@ -1,31 +0,0 @@
1
- # @equinor/apollo-utils
2
-
3
- ## 0.1.3
4
-
5
- ### Patch Changes
6
-
7
- - e1354f0: Move edit cells to apollo-utils
8
- - Updated dependencies [e1354f0]
9
- - @equinor/apollo-components@3.1.2
10
-
11
- ## 0.1.2
12
-
13
- ### Patch Changes
14
-
15
- - 7fa24ff: Move cells to @equinor/apollo-utils and extract common functionality into separate package
16
- - Updated dependencies [07e344a]
17
- - Updated dependencies [7fa24ff]
18
- - apollo-common@0.1.2
19
-
20
- ## 0.1.1
21
-
22
- ### Patch Changes
23
-
24
- - f9e695e: Add validateAsync utility to support async refinements
25
-
26
- ## 0.1.0
27
-
28
- ### Minor Changes
29
-
30
- - 8abf3eb: Initial version of Apollo Utils with jotai-form
31
- - 8abf3eb: Create apollo-utils package and jotai form example app
@@ -1,34 +0,0 @@
1
- import { Checkbox, EdsProvider } from '@equinor/eds-core-react'
2
- import { CellContext } from '@tanstack/react-table'
3
- import { Controller, useFormContext } from 'react-hook-form'
4
- import { FormMeta, useEditMode } from '../form-meta'
5
-
6
- export function EditableCheckboxCell<T extends FormMeta>(context: CellContext<T, boolean>) {
7
- const editMode = useEditMode()
8
- const { control } = useFormContext()
9
-
10
- if (!editMode)
11
- return (
12
- <EdsProvider density="compact">
13
- <Checkbox
14
- enterKeyHint="next"
15
- aria-label="readonly"
16
- readOnly={true}
17
- checked={context.getValue()}
18
- disabled={true}
19
- />
20
- </EdsProvider>
21
- )
22
-
23
- return (
24
- <Controller
25
- control={control}
26
- name={context.column.id}
27
- render={({ field: { value, ...field } }) => (
28
- <EdsProvider density="compact">
29
- <Checkbox enterKeyHint="send" aria-label="editable" checked={value} {...field} />
30
- </EdsProvider>
31
- )}
32
- />
33
- )
34
- }
@@ -1,80 +0,0 @@
1
- import { TypographyCustom } from '@equinor/apollo-components'
2
- import { TextField } from '@equinor/eds-core-react'
3
- import { CellContext } from '@tanstack/react-table'
4
- import { ChangeEvent, useMemo } from 'react'
5
- import { Controller, useFormContext } from 'react-hook-form'
6
- import styled from 'styled-components'
7
- import { FormMeta, useEditMode } from '../form-meta'
8
- import { getHelperTextProps } from './utils'
9
-
10
- export interface EditableDateCellProps<T extends FormMeta> extends CellContext<T, unknown> {
11
- dateStringFormatter?: (date: string) => string
12
- }
13
-
14
- export function EditableDateCell<T extends FormMeta>(props: EditableDateCellProps<T>) {
15
- const { dateStringFormatter, ...context } = props
16
- const rawValue = context.getValue<string>()
17
-
18
- const editMode = useEditMode()
19
- const { control } = useFormContext()
20
-
21
- const formattedValue = useMemo(
22
- () => dateStringFormatter?.(rawValue) ?? rawValue,
23
- [rawValue, dateStringFormatter]
24
- )
25
-
26
- if (!editMode) return <TypographyCustom truncate>{formattedValue}</TypographyCustom>
27
-
28
- return (
29
- <Controller
30
- control={control}
31
- name={context.column.id}
32
- render={({ field: { value, onChange, ...field }, fieldState: { error } }) => (
33
- <InlineTextField
34
- id={context.column.id}
35
- type="date"
36
- autoComplete="none"
37
- value={value ? parseISODate(value) : ''}
38
- onChange={(e: ChangeEvent<HTMLInputElement>) =>
39
- onChange(e.target.value ? parseISODate(e.target.value) : '')
40
- }
41
- {...getHelperTextProps({ error })}
42
- {...field}
43
- />
44
- )}
45
- />
46
- )
47
- }
48
-
49
- /**
50
- * Formats a date string into `yyyy-mm-dd` format.
51
- */
52
- function parseISODate(dateString: string) {
53
- const date = new Date(dateString)
54
- const offset = date.getTimezoneOffset()
55
- const dateWithoutOffset = new Date(date.getTime() - offset * 60 * 1000)
56
- return dateWithoutOffset.toISOString().split('T')[0]
57
- }
58
-
59
- const InlineTextField = styled(TextField)`
60
- /*
61
- TODO: Improve input based on feedback from UX
62
- & > div {
63
- background: transparent;
64
- margin: 0 -0.5rem;
65
- padding: 0 0.5rem;
66
- box-shadow: none;
67
- width: auto;
68
- }
69
-
70
- input {
71
- padding: 0;
72
- letter-spacing: 0;
73
- font-weight: 400;
74
- color: inherit;
75
-
76
- ::placeholder {
77
- color: red;
78
- }
79
- } */
80
- `
@@ -1,68 +0,0 @@
1
- import { Autocomplete } from '@equinor/eds-core-react'
2
- import { CellContext } from '@tanstack/react-table'
3
- import { Controller, useFormContext } from 'react-hook-form'
4
- import styled from 'styled-components'
5
- import { FormMeta, useEditMode } from '../form-meta'
6
- import { TypographyCustom } from './TypographyCustom'
7
-
8
- export interface Option {
9
- label: string
10
- value: string
11
- }
12
-
13
- export interface EditableDropdownCellProps<T extends FormMeta> extends CellContext<T, unknown> {
14
- /**
15
- * `Option.value` is used internally to get and update selection state. `Option.label` is *only* for visual purposes.
16
- */
17
- options: Option[]
18
- }
19
-
20
- function buildEmptyOption(): Option {
21
- return { label: '', value: '' }
22
- }
23
-
24
- export function EditableDropdownCell<T extends FormMeta>(props: EditableDropdownCellProps<T>) {
25
- const { options, ...context } = props
26
- const editMode = useEditMode()
27
- const { control } = useFormContext()
28
- if (!editMode) return <TypographyCustom truncate>{context.getValue() as any}</TypographyCustom>
29
-
30
- return (
31
- <Controller
32
- control={control}
33
- name={context.column.id}
34
- render={({ field: { value, onChange, ...field } }) => {
35
- const selectedOption = options.find((option) => option.value === value) ?? buildEmptyOption
36
- return (
37
- <AutocompleteCustom
38
- label=""
39
- // Casting is due to stying the Autocomplete, plain EDS Autocomplete works
40
- // Fixed when workaround is not needed anymore
41
- selectedOptions={selectedOption && ([selectedOption] as Option[])}
42
- options={options}
43
- optionLabel={(option) => (option as Option)?.label ?? ''}
44
- aria-required
45
- hideClearButton
46
- aria-autocomplete="none"
47
- onOptionsChange={(changes) => {
48
- const [change] = changes.selectedItems
49
- onChange((change as Option)?.value)
50
- }}
51
- {...field}
52
- />
53
- )
54
- }}
55
- />
56
- )
57
- }
58
-
59
- // Requested in https://github.com/equinor/design-system/issues/2804
60
- export const AutocompleteCustom = styled(Autocomplete)`
61
- input[type='text'] {
62
- overflow: hidden;
63
- white-space: nowrap;
64
- text-overflow: ellipsis;
65
- padding-right: ${({ hideClearButton }) =>
66
- hideClearButton ? `calc(8px + (24px * 1))` : `calc(8px + (24px * 2))`};
67
- }
68
- `
@@ -1,57 +0,0 @@
1
- import { TextField } from '@equinor/eds-core-react'
2
- import { CellContext } from '@tanstack/react-table'
3
- import { ChangeEvent } from 'react'
4
- import { Controller, useFormContext } from 'react-hook-form'
5
- import styled from 'styled-components'
6
- import { FormMeta, useEditMode } from '../form-meta'
7
- import { TypographyCustom } from './TypographyCustom'
8
- import { getHelperTextProps } from './utils'
9
-
10
- export function EditableNumberCell<T extends FormMeta>(context: CellContext<T, number>) {
11
- const editMode = useEditMode()
12
- const { control } = useFormContext()
13
-
14
- if (!editMode) return <TypographyCustom truncate>{context.getValue()}</TypographyCustom>
15
-
16
- return (
17
- <Controller
18
- control={control}
19
- name={context.column.id}
20
- render={({ field: { onChange, ...field }, fieldState: { error } }) => (
21
- <>
22
- <InlineTextField
23
- id={context.column.id}
24
- type="number"
25
- autoComplete="none"
26
- onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.valueAsNumber)}
27
- {...field}
28
- {...getHelperTextProps({ error })}
29
- />
30
- </>
31
- )}
32
- />
33
- )
34
- }
35
-
36
- const InlineTextField = styled(TextField)`
37
- /*
38
- TODO: Improve input based on feedback from UX
39
- & > div {
40
- background: transparent;
41
- margin: 0 -0.5rem;
42
- padding: 0 0.5rem;
43
- box-shadow: none;
44
- width: auto;
45
- }
46
-
47
- input {
48
- padding: 0;
49
- letter-spacing: 0;
50
- font-weight: 400;
51
- color: inherit;
52
-
53
- ::placeholder {
54
- color: red;
55
- }
56
- } */
57
- `
@@ -1,126 +0,0 @@
1
- import { Button, Dialog as EDS, Icon, TextField } from '@equinor/eds-core-react'
2
- import { CellContext } from '@tanstack/react-table'
3
- import { ChangeEvent, useState } from 'react'
4
- import { Controller, useFormContext } from 'react-hook-form'
5
- import styled from 'styled-components'
6
- import { FormMeta, useEditMode } from '../form-meta'
7
- import { PopoverCell } from './PopoverCell'
8
- import { getHelperTextProps, stopPropagation } from './utils'
9
-
10
- interface EdtiableTextAreaProps<T extends FormMeta> extends CellContext<T, string> {
11
- title: string
12
- }
13
-
14
- export function EditableTextAreaCell<T extends FormMeta>(props: EdtiableTextAreaProps<T>) {
15
- const { title, ...context } = props
16
-
17
- const [textareaValue, setTextareaValue] = useState<string>(context.getValue())
18
- const [open, setOpen] = useState(false)
19
- const editMode = useEditMode()
20
- const { control } = useFormContext()
21
-
22
- const name = context.column.id
23
-
24
- if (!editMode)
25
- return <PopoverCell id={name + 'popover'} value={context.getValue()} title={title} />
26
-
27
- const openDialog = () => setOpen(true)
28
- const closeDialog = () => setOpen(false)
29
-
30
- return (
31
- <Controller
32
- control={control}
33
- name={name}
34
- render={({ field: { onChange, ref, ...field }, fieldState: { error } }) => (
35
- <>
36
- {/* Inline input */}
37
- <div
38
- style={{
39
- display: 'flex',
40
- alignItems: 'center',
41
- position: 'relative',
42
- }}
43
- >
44
- <StyledTextField
45
- id={field.name}
46
- onChange={onChange}
47
- ref={ref}
48
- {...field}
49
- {...getHelperTextProps({ error })}
50
- />
51
- <IconButton variant="ghost_icon" onClick={stopPropagation(openDialog)}>
52
- <Icon name="arrow_up" size={24} style={{ transform: 'rotateZ(45deg)' }} />
53
- </IconButton>
54
- </div>
55
-
56
- {/* Dialog */}
57
- <EDS
58
- open={open}
59
- onClose={() => {
60
- closeDialog()
61
- onChange(textareaValue)
62
- }}
63
- isDismissable
64
- style={{ width: 'min(50rem, calc(100vw - 4rem))' }}
65
- >
66
- <EDS.Header>
67
- <EDS.Title>{title}</EDS.Title>
68
- </EDS.Header>
69
- <EDS.Content>
70
- <TextField
71
- style={{
72
- maxWidth: '100%',
73
- marginTop: '1rem',
74
- }}
75
- id={field.name + 'modal'}
76
- multiline
77
- rows={8}
78
- name={field.name + 'modal'}
79
- value={textareaValue}
80
- onChange={(e: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>) => {
81
- setTextareaValue(e.target.value)
82
- }}
83
- />
84
- </EDS.Content>
85
- <EDS.Actions style={{ display: 'flex', gap: '1rem' }}>
86
- <Button
87
- onClick={() => {
88
- closeDialog()
89
- onChange(textareaValue)
90
- }}
91
- >
92
- Submit
93
- </Button>
94
- <Button
95
- variant="ghost"
96
- onClick={() => {
97
- closeDialog()
98
- setTextareaValue(context.getValue())
99
- }}
100
- >
101
- Cancel
102
- </Button>
103
- </EDS.Actions>
104
- </EDS>
105
- </>
106
- )}
107
- />
108
- )
109
- }
110
-
111
- const StyledTextField = styled(TextField)`
112
- & input {
113
- padding-right: 40px;
114
- letter-spacing: 0;
115
- overflow: hidden;
116
- white-space: nowrap;
117
- text-overflow: ellipsis;
118
- }
119
- `
120
-
121
- const IconButton = styled(Button)`
122
- position: absolute;
123
- height: 32px;
124
- width: 32px;
125
- right: 4px;
126
- `
@@ -1,53 +0,0 @@
1
- import { TextField } from '@equinor/eds-core-react'
2
- import { CellContext } from '@tanstack/react-table'
3
- import { Controller, FieldPath, useFormContext } from 'react-hook-form'
4
- import styled from 'styled-components'
5
- import { FormMeta, useEditMode } from '../form-meta'
6
- import { TypographyCustom } from './TypographyCustom'
7
- import { getHelperTextProps } from './utils'
8
-
9
- export function EditableTextFieldCell<T extends FormMeta>(context: CellContext<T, unknown>) {
10
- const { control } = useFormContext<T>()
11
- const editMode = useEditMode()
12
-
13
- if (!editMode) return <TypographyCustom truncate>{context.getValue() as any}</TypographyCustom>
14
-
15
- return (
16
- <Controller
17
- control={control}
18
- name={context.column.id as FieldPath<T>}
19
- render={({ field: { value, ...field }, fieldState: { error } }) => (
20
- <InlineTextField
21
- id={context.column.id}
22
- autoComplete="none"
23
- value={String(value)}
24
- {...field}
25
- {...getHelperTextProps({ error })}
26
- />
27
- )}
28
- />
29
- )
30
- }
31
-
32
- const InlineTextField = styled(TextField)`
33
- /*
34
- TODO: Improve input based on feedback from UX
35
- & > div {
36
- background: transparent;
37
- margin: 0 -0.5rem;
38
- padding: 0 0.5rem;
39
- box-shadow: none;
40
- width: auto;
41
- }
42
-
43
- input {
44
- padding: 0;
45
- letter-spacing: 0;
46
- font-weight: 400;
47
- color: inherit;
48
-
49
- ::placeholder {
50
- color: red;
51
- }
52
- } */
53
- `
@@ -1,42 +0,0 @@
1
- import { Popover, Typography } from '@equinor/eds-core-react'
2
- import { ReactNode, useRef, useState } from 'react'
3
- import { TypographyCustom } from './TypographyCustom'
4
- import { stopPropagation } from './utils'
5
-
6
- interface PopoverCellProps {
7
- id: string
8
- value: string
9
- title?: string | JSX.Element | ReactNode
10
- }
11
-
12
- export const PopoverCell = (props: PopoverCellProps) => {
13
- const [open, setOpen] = useState(false)
14
- const anchorRef = useRef<HTMLDivElement>(null)
15
- const handleClick = () => setOpen(!open)
16
- const handleClose = () => setOpen(false)
17
-
18
- return (
19
- <div style={{ position: 'relative' }} ref={anchorRef}>
20
- <TypographyCustom truncate onClick={stopPropagation(handleClick)}>
21
- {String(props.value)}
22
- </TypographyCustom>
23
- <Popover
24
- id={props.id}
25
- open={open}
26
- aria-controls="expand cell"
27
- anchorEl={anchorRef.current}
28
- onClose={handleClose}
29
- placement={'bottom'}
30
- >
31
- {props.title && (
32
- <Popover.Title>
33
- <Popover.Header>{props.title}</Popover.Header>
34
- </Popover.Title>
35
- )}
36
- <Popover.Content>
37
- <Typography>{String(props.value)}</Typography>
38
- </Popover.Content>
39
- </Popover>
40
- </div>
41
- )
42
- }
@@ -1,62 +0,0 @@
1
- import {
2
- Typography as EdsTypography,
3
- TypographyProps as EdsTypographyProps,
4
- } from '@equinor/eds-core-react'
5
- import { tokens } from '@equinor/eds-tokens'
6
- import { CSSProperties } from 'react'
7
- import styled from 'styled-components'
8
-
9
- export type TypographyProps = {
10
- truncate?: boolean
11
- } & EdsTypographyProps
12
-
13
- const truncateStyle: CSSProperties = {
14
- overflow: 'hidden',
15
- whiteSpace: 'nowrap',
16
- textOverflow: 'ellipsis',
17
- }
18
-
19
- export const TypographyCustom = (props: TypographyProps) => {
20
- const { truncate, style: styleFromProps, ...edsTypographyProps } = props
21
-
22
- if (truncate)
23
- return (
24
- <HoverCapture>
25
- <EdsTypography
26
- {...edsTypographyProps}
27
- style={{
28
- ...styleFromProps,
29
- ...truncateStyle,
30
- }}
31
- />
32
- </HoverCapture>
33
- )
34
-
35
- return <EdsTypography {...edsTypographyProps} style={styleFromProps} />
36
- }
37
-
38
- const HoverCapture = styled.div`
39
- height: ${tokens.typography.table.cell_text.lineHeight};
40
- background-color: inherit;
41
-
42
- position: relative;
43
- width: 100%;
44
-
45
- &:hover {
46
- z-index: 1;
47
- }
48
-
49
- & > * {
50
- width: inherit;
51
- position: absolute;
52
- }
53
-
54
- &:hover > * {
55
- width: auto;
56
- z-index: 1;
57
- padding: 0.5em 1em;
58
- margin: -0.5em -1em;
59
-
60
- background-color: inherit;
61
- }
62
- `
@@ -1,6 +0,0 @@
1
- export * from './EditableCheckboxCell'
2
- export * from './EditableDateCell'
3
- export * from './EditableDropdownCell'
4
- export * from './EditableNumberCell'
5
- export * from './EditableTextAreaCell'
6
- export * from './EditableTextFieldCell'
@@ -1,49 +0,0 @@
1
- import { Icon } from '@equinor/eds-core-react'
2
- import { Variants } from '@equinor/eds-core-react/dist/types/components/types'
3
- import { error_filled, warning_filled } from '@equinor/eds-icons'
4
- import { SyntheticEvent } from 'react'
5
-
6
- interface GetHelperTextPropsProps {
7
- error?: { message?: string }
8
- warning?: { message: string }
9
- helperText?: string
10
- }
11
-
12
- interface GetHelperTextProps {
13
- variant?: Variants
14
- helperText?: string
15
- helperIcon: JSX.Element | null
16
- }
17
-
18
- export function getHelperTextProps({
19
- error,
20
- warning,
21
- helperText,
22
- }: GetHelperTextPropsProps): GetHelperTextProps {
23
- if (error)
24
- return {
25
- variant: 'error',
26
- helperText: error.message,
27
- helperIcon: <Icon data={error_filled} size={16} />,
28
- }
29
-
30
- if (warning)
31
- return {
32
- variant: 'warning',
33
- helperText: warning.message,
34
- helperIcon: <Icon data={warning_filled} size={16} />,
35
- }
36
-
37
- return {
38
- helperText,
39
- helperIcon: null,
40
- }
41
- }
42
-
43
- /** Wrap an event handler and stop event propagation */
44
- export function stopPropagation<T extends HTMLElement>(handler: (e: SyntheticEvent<T>) => void) {
45
- return (e: SyntheticEvent<T>) => {
46
- e.stopPropagation()
47
- handler(e)
48
- }
49
- }
@@ -1,2 +0,0 @@
1
- export * from './types'
2
- export * from './utils'
@@ -1,7 +0,0 @@
1
- export type FormMeta = {
2
- _isNew?: boolean;
3
- _editMode?: boolean;
4
- _hasRemoteChange?: boolean;
5
- };
6
-
7
- export type WithoutFormMeta<T extends FormMeta> = Omit<T, keyof FormMeta>;
@@ -1,66 +0,0 @@
1
- import { omit } from 'lodash'
2
- import { useFormContext } from 'react-hook-form'
3
- import { FormMeta } from './types'
4
-
5
- // This file contains methods related to the FormMeta type
6
-
7
- const formMeta: FormMeta = {
8
- _editMode: false,
9
- _isNew: false,
10
- _hasRemoteChange: false,
11
- }
12
-
13
- /**
14
- * Subscribes to the `_editMode` field in a `react-hook-form` context.
15
- *
16
- * @returns edit mode value
17
- */
18
- export function useEditMode() {
19
- const { watch } = useFormContext<FormMeta>()
20
- return watch('_editMode') ?? false
21
- }
22
-
23
- /**
24
- * Subscribes to the `_hasRemoteChange` field in a `react-hook-form` context.
25
- *
26
- * @returns edit mode value
27
- */
28
- export function useHasRemoteChange() {
29
- const { watch } = useFormContext<FormMeta>()
30
- return watch('_hasRemoteChange') ?? false
31
- }
32
-
33
- /**
34
- * @returns function getting is new meta
35
- */
36
- export function useGetIsNew() {
37
- const { getValues } = useFormContext<FormMeta>()
38
- return () => getValues('_isNew') ?? false
39
- }
40
-
41
- export function useSetFormMeta<T extends FormMeta>() {
42
- const { setValue } = useFormContext<T>()
43
- return (newValues: Partial<T>) =>
44
- objectKeys(newValues).forEach((key) => newValues[key] && setValue(key, newValues[key]))
45
- }
46
-
47
- /**
48
- * ```
49
- * Object.keys()
50
- * ```
51
- * With better typing. Same uses.
52
- *
53
- * @param obj
54
- * @returns `Object.keys(obj)`
55
- */
56
- function objectKeys<Obj>(obj: any): (keyof Obj)[] {
57
- return Object.keys(obj) as (keyof Obj)[]
58
- }
59
-
60
- export function removeFormMeta<T extends FormMeta>(withFormMeta: T): Omit<T, keyof FormMeta> {
61
- return omit(withFormMeta, Object.keys(formMeta)) as Omit<T, keyof FormMeta>
62
- }
63
-
64
- export function addFormMeta<T>(withoutFormMeta: T): T & FormMeta {
65
- return { ...formMeta, ...withoutFormMeta }
66
- }
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from './edit-cells'
2
- export * from './form-meta'
3
- export * from './jotai-form'
4
- export * from './zod-validation'
@@ -1,36 +0,0 @@
1
- import { renderHook } from '@testing-library/react-hooks'
2
- import { useAtomValue } from 'jotai'
3
- import { describe, expect, it } from 'vitest'
4
- import { examplePerson, Person } from '../test-data/person'
5
- import { createFormFamily, useFormFamilyUtils } from './formUtils'
6
-
7
- describe('Test createFormFamily', () => {
8
- it('should create a family', () => {
9
- const family = createFormFamily<Person>()
10
- expect(family).toBeTruthy()
11
- })
12
- it('should initialize atom as undefined', () => {
13
- const family = createFormFamily<Person>()
14
- const { result } = renderHook(() => useAtomValue(family(examplePerson)))
15
- expect(result.current).toBeUndefined()
16
- })
17
- })
18
-
19
- function initFormUtils() {
20
- const family = createFormFamily<Person>()
21
- return useFormFamilyUtils(family)
22
- }
23
-
24
- describe('Test useFormFamilyUtils', () => {
25
- it('should initialize form as valid with values', () => {
26
- const utils = initFormUtils()
27
- renderHook(() => utils.useFormMutations(examplePerson).initializeForm(examplePerson))
28
- const { result } = renderHook(() => utils.useFormState(examplePerson))
29
- expect(result.current?.isValid).toBeTruthy()
30
- expect(result.current).toStrictEqual({
31
- status: 'editing',
32
- values: examplePerson,
33
- isValid: true,
34
- })
35
- })
36
- })
@@ -1,73 +0,0 @@
1
- import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from 'jotai'
2
- import { atomFamily } from 'jotai/utils'
3
- import { type AtomFamily } from 'jotai/vanilla/utils/atomFamily'
4
- import { createValidator } from '../zod-validation'
5
- import { FormFamilyParam, FormState } from './types'
6
-
7
- export function createFormFamily<E extends Record<string, unknown>>() {
8
- return atomFamily(
9
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
10
- (_param: FormFamilyParam) => atom<FormState<E> | undefined>(undefined),
11
- (a, b) => a.id === b.id
12
- )
13
- }
14
-
15
- type FormFamily<T> = AtomFamily<FormFamilyParam, PrimitiveAtom<FormState<T> | undefined>>
16
- type FormValidator = ReturnType<typeof createValidator>
17
-
18
- type UseFormFamilyUtilsOptions = {
19
- validator?: FormValidator
20
- }
21
-
22
- export function useFormFamilyUtils<T>(
23
- family: FormFamily<T>,
24
- options: UseFormFamilyUtilsOptions = {}
25
- ) {
26
- return {
27
- useFormStateAtom: (param: FormFamilyParam) => useAtom(family(param)),
28
- useFormState: (param: FormFamilyParam) => useAtomValue(family(param)),
29
- useUpdateFormMutation: (param: FormFamilyParam) => {
30
- return useFormFamilyMutation(family, param, options.validator)
31
- },
32
- useFormMutations(param: FormFamilyParam) {
33
- const mutateAtom = useSetAtom(family(param))
34
-
35
- return {
36
- update: useFormFamilyMutation(family, param, options.validator),
37
- initializeForm: (entity: T) =>
38
- mutateAtom({
39
- status: 'editing',
40
- values: entity,
41
- isValid: true,
42
- }),
43
- resetForm: () => mutateAtom(undefined),
44
- }
45
- },
46
- }
47
- }
48
-
49
- export function useFormFamilyMutation<T>(
50
- family: FormFamily<T>,
51
- param: FormFamilyParam,
52
- validator?: FormValidator
53
- ) {
54
- const mutate = useSetAtom(family(param))
55
- return (update: Partial<T>) => {
56
- return mutate((previous) => {
57
- if (!previous) return
58
-
59
- const updatedValues = {
60
- ...previous.values,
61
- ...update,
62
- }
63
- const errors = validator?.validate(updatedValues)
64
-
65
- return {
66
- status: 'editing',
67
- values: updatedValues,
68
- errors,
69
- isValid: !errors,
70
- }
71
- })
72
- }
73
- }
@@ -1,2 +0,0 @@
1
- export * from './formUtils'
2
- export * from './types'
@@ -1,12 +0,0 @@
1
- import { ValidationErrorMap } from '../zod-validation'
2
-
3
- export type FormState<T> = {
4
- status: 'editing' | 'pending'
5
- values: T
6
- errors?: ValidationErrorMap<T>
7
- isValid?: boolean
8
- }
9
-
10
- export type FormFamilyParam = {
11
- id: number | string
12
- } & Record<string, unknown>
@@ -1,15 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export const personSchema = z.object({
4
- id: z.number(),
5
- name: z.string().min(1),
6
- age: z.number().min(0).max(150),
7
- })
8
-
9
- export type Person = z.infer<typeof personSchema>
10
-
11
- export const examplePerson: Person = {
12
- id: 0,
13
- name: 'Apollo',
14
- age: 25,
15
- }
@@ -1,2 +0,0 @@
1
- export * from './types'
2
- export * from './utils'
@@ -1 +0,0 @@
1
- export type ValidationErrorMap<T> = Map<keyof T, { message: string; code: string }>
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { examplePerson, personSchema } from '../test-data/person'
3
- import { createValidator } from './utils'
4
-
5
- const validator = createValidator(personSchema)
6
-
7
- describe('Test validator creation', () => {
8
- it('should return undefined on valid data', () => {
9
- const errors = validator.validate(examplePerson)
10
- expect(errors).toBeUndefined()
11
- })
12
- it('should return errors on invalid name and age', () => {
13
- const errors = validator.validate({ ...examplePerson, name: '', age: 200 })
14
- expect(errors?.has('name')).toBeTruthy()
15
- expect(errors?.has('age')).toBeTruthy()
16
- })
17
- })
@@ -1,29 +0,0 @@
1
- import { z } from 'zod'
2
- import { ValidationErrorMap } from './types'
3
-
4
- export function createValidator<S extends z.ZodTypeAny>(schema: S) {
5
- return {
6
- validate: <E extends z.infer<typeof schema>>(entity: E) => {
7
- const validation = schema.safeParse(entity)
8
- if (validation.success) return undefined
9
- return prepareErrors<E>(validation)
10
- },
11
- validateAsync: async <E extends z.infer<typeof schema>>(entity: z.infer<typeof schema>) => {
12
- const validation = await schema.safeParseAsync(entity)
13
- if (validation.success) return undefined
14
- return prepareErrors<E>(validation)
15
- },
16
- getSchema() {
17
- return schema
18
- },
19
- }
20
- }
21
-
22
- function prepareErrors<E extends Record<string, unknown>>(errorValidation: z.SafeParseError<E>) {
23
- return new Map(
24
- errorValidation.error.errors.map((error) => [
25
- error.path[0] as keyof E,
26
- { message: error.message, code: error.code },
27
- ])
28
- ) as ValidationErrorMap<E>
29
- }
package/tsconfig.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "tsconfig/vite.json",
3
- "include": ["src", "setupTest.ts"],
4
- "compilerOptions": { "types": ["vitest/importMeta"] }
5
- }
package/vitest.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from 'vitest/config'
2
-
3
- export default defineConfig({
4
- define: {
5
- 'import.meta.vitest': 'undefined',
6
- },
7
- test: {
8
- includeSource: ['src/**/*.ts'],
9
- environment: 'jsdom',
10
- },
11
- })