@equinor/apollo-utils 0.1.1 → 0.1.2

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.
@@ -5,10 +5,10 @@ $ tsup src/index.ts --format esm,cjs --dts --external react
5
5
  CLI Target: es6
6
6
  ESM Build start
7
7
  CJS Build start
8
- ESM dist/index.mjs 3.48 KB
9
- ESM ⚡️ Build success in 85ms
10
- CJS dist/index.js 4.62 KB
11
- CJS ⚡️ Build success in 86ms
8
+ CJS dist/index.js 4.83 KB
9
+ CJS ⚡️ Build success in 103ms
10
+ ESM dist/index.mjs 3.53 KB
11
+ ESM ⚡️ Build success in 107ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 4462ms
14
- DTS dist/index.d.ts 2.00 KB
13
+ DTS ⚡️ Build success in 6256ms
14
+ DTS dist/index.d.ts 2.03 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @equinor/apollo-utils
2
2
 
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 7fa24ff: Move cells to @equinor/apollo-utils and extract common functionality into separate package
8
+ - Updated dependencies [07e344a]
9
+ - Updated dependencies [7fa24ff]
10
+ - apollo-common@0.1.2
11
+
3
12
  ## 0.1.1
4
13
 
5
14
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from 'apollo-common';
1
2
  import { PrimitiveAtom } from 'jotai';
2
3
  import { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
3
4
  import { z } from 'zod';
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var __copyProps = (to, from, except, desc) => {
29
29
  }
30
30
  return to;
31
31
  };
32
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
32
33
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
34
  var __async = (__this, __arguments, generator) => {
34
35
  return new Promise((resolve, reject) => {
@@ -60,6 +61,7 @@ __export(src_exports, {
60
61
  useFormFamilyUtils: () => useFormFamilyUtils
61
62
  });
62
63
  module.exports = __toCommonJS(src_exports);
64
+ __reExport(src_exports, require("apollo-common"), module.exports);
63
65
 
64
66
  // src/jotai-form/formUtils.ts
65
67
  var import_jotai = require("jotai");
package/dist/index.mjs CHANGED
@@ -35,6 +35,9 @@ var __async = (__this, __arguments, generator) => {
35
35
  });
36
36
  };
37
37
 
38
+ // src/index.ts
39
+ export * from "apollo-common";
40
+
38
41
  // src/jotai-form/formUtils.ts
39
42
  import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
40
43
  import { atomFamily } from "jotai/utils";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@equinor/apollo-utils",
3
3
  "main": "src/index.ts",
4
4
  "types": "src/index.ts",
5
- "version": "0.1.1",
5
+ "version": "0.1.2",
6
6
  "license": "MIT",
7
7
  "scripts": {
8
8
  "build": "tsup src/index.ts --format esm,cjs --dts --external react",
@@ -17,11 +17,17 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
+ "apollo-common": "*",
20
21
  "jotai": "^2.0.1",
21
22
  "tsup": "^6.6.3",
22
23
  "typescript": "^4.9.5",
23
24
  "zod": "^3.20.6"
24
25
  },
26
+ "peerDependencies": {
27
+ "react": "^18.2.0",
28
+ "react-dom": "^18.2.0",
29
+ "react-hook-form": "^7.43.8"
30
+ },
25
31
  "devDependencies": {
26
32
  "@testing-library/react": "^13.4.0",
27
33
  "@testing-library/react-hooks": "^8.0.1",
@@ -31,6 +37,7 @@
31
37
  "jsdom": "^21.1.0",
32
38
  "react": "^18.2.0",
33
39
  "react-dom": "^18.2.0",
40
+ "react-hook-form": "^7.43.8",
34
41
  "vitest": "^0.28.5"
35
42
  }
36
43
  }
@@ -0,0 +1,34 @@
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
+ }
@@ -0,0 +1,80 @@
1
+ import { TextField } from '@equinor/eds-core-react'
2
+ import { CellContext } from '@tanstack/react-table'
3
+ import { TypographyCustom } from 'apollo-common'
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
+ `
@@ -0,0 +1,68 @@
1
+ import { Autocomplete } from '@equinor/eds-core-react'
2
+ import { CellContext } from '@tanstack/react-table'
3
+ import { TypographyCustom } from 'apollo-common'
4
+ import { Controller, useFormContext } from 'react-hook-form'
5
+ import styled from 'styled-components'
6
+ import { FormMeta, useEditMode } from '../form-meta'
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
+ `
@@ -0,0 +1,57 @@
1
+ import { TextField } from '@equinor/eds-core-react'
2
+ import { CellContext } from '@tanstack/react-table'
3
+ import { TypographyCustom } from 'apollo-common'
4
+ import { ChangeEvent } 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 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
+ `
@@ -0,0 +1,126 @@
1
+ import { Button, Dialog as EDS, Icon, TextField } from '@equinor/eds-core-react'
2
+ import { CellContext } from '@tanstack/react-table'
3
+ import { PopoverCell, stopPropagation } from 'apollo-common'
4
+ import { ChangeEvent, useState } 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
+ interface EdtiableTextAreaProps<T extends FormMeta> extends CellContext<T, string> {
11
+ dialogTitle: string
12
+ }
13
+
14
+ export function EditableTextAreaCell<T extends FormMeta>(props: EdtiableTextAreaProps<T>) {
15
+ const { dialogTitle, ...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="Comment" />
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>{dialogTitle}</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
+ `
@@ -0,0 +1,53 @@
1
+ import { TextField } from '@equinor/eds-core-react'
2
+ import { CellContext } from '@tanstack/react-table'
3
+ import { TypographyCustom } from 'apollo-common'
4
+ import { Controller, FieldPath, useFormContext } from 'react-hook-form'
5
+ import styled from 'styled-components'
6
+ import { FormMeta, useEditMode } from '../form-meta'
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
+ `
@@ -0,0 +1,6 @@
1
+ export * from './EditableCheckboxCell'
2
+ export * from './EditableDateCell'
3
+ export * from './EditableDropdownCell'
4
+ export * from './EditableNumberCell'
5
+ export * from './EditableTextAreaCell'
6
+ export * from './EditableTextFieldCell'
@@ -0,0 +1,40 @@
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
+
5
+ interface GetHelperTextPropsProps {
6
+ error?: { message?: string }
7
+ warning?: { message: string }
8
+ helperText?: string
9
+ }
10
+
11
+ interface GetHelperTextProps {
12
+ variant?: Variants
13
+ helperText?: string
14
+ helperIcon: JSX.Element | null
15
+ }
16
+
17
+ export function getHelperTextProps({
18
+ error,
19
+ warning,
20
+ helperText,
21
+ }: GetHelperTextPropsProps): GetHelperTextProps {
22
+ if (error)
23
+ return {
24
+ variant: 'error',
25
+ helperText: error.message,
26
+ helperIcon: <Icon data={error_filled} size={16} />,
27
+ }
28
+
29
+ if (warning)
30
+ return {
31
+ variant: 'warning',
32
+ helperText: warning.message,
33
+ helperIcon: <Icon data={warning_filled} size={16} />,
34
+ }
35
+
36
+ return {
37
+ helperText,
38
+ helperIcon: null,
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export * from './utils'
@@ -0,0 +1,7 @@
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>;
@@ -0,0 +1,66 @@
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 CHANGED
@@ -1,2 +1,3 @@
1
+ export * from 'apollo-common'
1
2
  export * from './jotai-form'
2
3
  export * from './zod-validation'