@dxos/plugin-status-bar 0.6.8-main.046e6cf
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/LICENSE +8 -0
- package/README.md +15 -0
- package/dist/lib/browser/chunk-PL5XPTPX.mjs +18 -0
- package/dist/lib/browser/chunk-PL5XPTPX.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +372 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/meta.mjs +9 -0
- package/dist/lib/browser/meta.mjs.map +7 -0
- package/dist/types/src/StatusBarPlugin.d.ts +4 -0
- package/dist/types/src/StatusBarPlugin.d.ts.map +1 -0
- package/dist/types/src/components/FeedbackForm.d.ts +5 -0
- package/dist/types/src/components/FeedbackForm.d.ts.map +1 -0
- package/dist/types/src/components/StatusBar.d.ts +42 -0
- package/dist/types/src/components/StatusBar.d.ts.map +1 -0
- package/dist/types/src/components/StatusBar.stories.d.ts +50 -0
- package/dist/types/src/components/StatusBar.stories.d.ts.map +1 -0
- package/dist/types/src/components/StatusBarImpl.d.ts +3 -0
- package/dist/types/src/components/StatusBarImpl.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useForm.d.ts +36 -0
- package/dist/types/src/hooks/useForm.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +15 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +13 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/validation.d.ts +7 -0
- package/dist/types/src/util/validation.d.ts.map +1 -0
- package/package.json +70 -0
- package/src/StatusBarPlugin.tsx +33 -0
- package/src/components/FeedbackForm.tsx +127 -0
- package/src/components/StatusBar.stories.tsx +43 -0
- package/src/components/StatusBar.tsx +110 -0
- package/src/components/StatusBarImpl.tsx +45 -0
- package/src/components/index.ts +6 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useForm.ts +119 -0
- package/src/index.ts +10 -0
- package/src/meta.tsx +18 -0
- package/src/translations.ts +40 -0
- package/src/util/index.ts +5 -0
- package/src/util/validation.ts +22 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { type ChangeEvent, useCallback, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { type ValidationError } from '../util';
|
|
7
|
+
|
|
8
|
+
// TODO(Zan): This module will be generally useful and we should factor it out after the
|
|
9
|
+
// API crystallizes a bit more.
|
|
10
|
+
|
|
11
|
+
interface FormOptions<T> {
|
|
12
|
+
initialValues: T;
|
|
13
|
+
validate: (values: any) => ValidationError[] | undefined;
|
|
14
|
+
onSubmit: (values: T) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a hook for managing form state, including values, validation, and submission.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const { values, handleChange, handleSubmit, errors, canSubmit, getInputProps } = useForm({
|
|
22
|
+
* initialValues: { name: '', email: '' },
|
|
23
|
+
* validate: (values) => { yourValidationLogic(values); },
|
|
24
|
+
* onSubmit: (values) => { submissionLogic(values); },
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export const useForm = <T extends object>({ initialValues, validate, onSubmit }: FormOptions<T>) => {
|
|
28
|
+
const [values, setValues] = useState<T>(initialValues);
|
|
29
|
+
const [errors, setErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>);
|
|
30
|
+
const [touched, setTouched] = useState<Record<keyof T, boolean>>(
|
|
31
|
+
Object.keys(initialValues).reduce((acc, key) => ({ ...acc, [key]: false }), {} as Record<keyof T, boolean>),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const runValidation = useCallback(
|
|
35
|
+
(values: T) => {
|
|
36
|
+
const validationErrors = validate(values);
|
|
37
|
+
setErrors(collapseErrorArray(validationErrors ?? []));
|
|
38
|
+
|
|
39
|
+
return validationErrors === undefined;
|
|
40
|
+
},
|
|
41
|
+
[validate],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const handleChange = useCallback(
|
|
45
|
+
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
46
|
+
const { name, value } = event.target;
|
|
47
|
+
const newValues = { ...values, [name]: value };
|
|
48
|
+
setValues(newValues);
|
|
49
|
+
runValidation(newValues);
|
|
50
|
+
},
|
|
51
|
+
[values, runValidation],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const touchAll = useCallback(() => setTouched(mkAllTouched(values)), [values, setTouched]);
|
|
55
|
+
|
|
56
|
+
const handleBlur = useCallback(
|
|
57
|
+
(event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
58
|
+
const { name } = event.target;
|
|
59
|
+
setTouched((touched) => ({ ...touched, [name]: true }));
|
|
60
|
+
|
|
61
|
+
// TODO(Zan): This should be configurable behavior.
|
|
62
|
+
if (event.relatedTarget?.getAttribute('type') === 'submit') {
|
|
63
|
+
// NOTE: We do this here instead of onSubmit, because the blur event is triggered before the submit event
|
|
64
|
+
// and results in the submit button being disabled when the form is invalid.
|
|
65
|
+
touchAll();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
runValidation(values);
|
|
69
|
+
},
|
|
70
|
+
[runValidation, values, touchAll],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const handleSubmit = useCallback(() => {
|
|
74
|
+
if (runValidation(values)) {
|
|
75
|
+
onSubmit(values);
|
|
76
|
+
}
|
|
77
|
+
}, [values, runValidation, onSubmit]);
|
|
78
|
+
|
|
79
|
+
// NOTE: We can submit if there is no touched field that has an error.
|
|
80
|
+
// - Basically, if there's a validation message present in the form, submit should be disabled.
|
|
81
|
+
const canSubmit = Object.keys(values).every((key) => touched[key as keyof T] === false || !errors[key as keyof T]);
|
|
82
|
+
|
|
83
|
+
const getInputProps = useCallback(
|
|
84
|
+
(key: keyof T) => ({
|
|
85
|
+
name: key,
|
|
86
|
+
value: values[key],
|
|
87
|
+
onChange: handleChange,
|
|
88
|
+
onBlur: handleBlur,
|
|
89
|
+
'aria-invalid': errors[key] !== undefined,
|
|
90
|
+
}),
|
|
91
|
+
[values, handleChange, handleBlur, errors],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
values,
|
|
96
|
+
handleChange,
|
|
97
|
+
handleSubmit,
|
|
98
|
+
errors,
|
|
99
|
+
canSubmit,
|
|
100
|
+
touched,
|
|
101
|
+
handleBlur,
|
|
102
|
+
onValidate: runValidation,
|
|
103
|
+
getInputProps,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const mkAllTouched = <T extends Record<keyof T, any>>(values: T) => {
|
|
108
|
+
return Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {} as Record<keyof T, boolean>);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const collapseErrorArray = <T>(errors: ValidationError[]) =>
|
|
112
|
+
errors.reduce(
|
|
113
|
+
(acc, { path, message }) => {
|
|
114
|
+
// TODO(Zan): This won't play well with nesting.
|
|
115
|
+
acc[path as keyof T] = message;
|
|
116
|
+
return acc;
|
|
117
|
+
},
|
|
118
|
+
{} as Record<keyof T, string>,
|
|
119
|
+
);
|
package/src/index.ts
ADDED
package/src/meta.tsx
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Info, type IconProps } from '@phosphor-icons/react';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { pluginMeta } from '@dxos/app-framework';
|
|
9
|
+
|
|
10
|
+
export const STATUS_BAR_PLUGIN = 'dxos.org/plugin/status-bar';
|
|
11
|
+
|
|
12
|
+
export default pluginMeta({
|
|
13
|
+
id: STATUS_BAR_PLUGIN,
|
|
14
|
+
name: 'Status Bar',
|
|
15
|
+
description: 'Display a bar with status and actions.',
|
|
16
|
+
iconComponent: (props: IconProps) => <Info {...props} />,
|
|
17
|
+
iconSymbol: 'ph--info--regular',
|
|
18
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type useTranslation } from '@dxos/react-ui';
|
|
6
|
+
|
|
7
|
+
import { STATUS_BAR_PLUGIN } from './meta';
|
|
8
|
+
|
|
9
|
+
type StatusBarTranslationKeys =
|
|
10
|
+
| 'plugin name'
|
|
11
|
+
| 'email input label'
|
|
12
|
+
| 'email input placeholder'
|
|
13
|
+
| 'feedback text area label'
|
|
14
|
+
| 'feedback text area placeholder'
|
|
15
|
+
| 'send feedback label'
|
|
16
|
+
| 'name label'
|
|
17
|
+
| 'name placeholder';
|
|
18
|
+
|
|
19
|
+
type RequiredTranslation = { [key in StatusBarTranslationKeys]: string };
|
|
20
|
+
type TranslationEntry = { [STATUS_BAR_PLUGIN]: RequiredTranslation };
|
|
21
|
+
type Language = string;
|
|
22
|
+
|
|
23
|
+
// -- helpers.
|
|
24
|
+
const entry = (translation: RequiredTranslation): TranslationEntry => ({ [STATUS_BAR_PLUGIN]: translation });
|
|
25
|
+
export const mkTranslation = (t: ReturnType<typeof useTranslation>['t']) => (key: StatusBarTranslationKeys) => t(key);
|
|
26
|
+
|
|
27
|
+
const translations: Record<Language, TranslationEntry> = {
|
|
28
|
+
'en-US': entry({
|
|
29
|
+
'plugin name': 'Status Bar',
|
|
30
|
+
'name label': 'Name',
|
|
31
|
+
'name placeholder': 'Preferred name',
|
|
32
|
+
'email input label': 'Email',
|
|
33
|
+
'email input placeholder': 'hello@example.com',
|
|
34
|
+
'feedback text area label': 'Feedback',
|
|
35
|
+
'feedback text area placeholder': 'Found a bug? Have a suggestion? Let us know!',
|
|
36
|
+
'send feedback label': 'Send Feedback',
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default [translations];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { ArrayFormatter } from '@effect/schema';
|
|
5
|
+
import { Effect } from 'effect';
|
|
6
|
+
|
|
7
|
+
import { S } from '@dxos/echo-schema';
|
|
8
|
+
|
|
9
|
+
export type ValidationError = { path: string; message: string };
|
|
10
|
+
|
|
11
|
+
export const validate = <T>(schema: S.Schema<T>, data: any): ValidationError[] | undefined => {
|
|
12
|
+
const validator = S.decodeUnknownEither(schema, { errors: 'all' });
|
|
13
|
+
const result = validator(data);
|
|
14
|
+
|
|
15
|
+
if (result._tag === 'Left') {
|
|
16
|
+
const errors = Effect.runSync(ArrayFormatter.formatError(result.left));
|
|
17
|
+
// TODO(Zan): Path.join is an assumption...
|
|
18
|
+
return errors.map(({ message, path }) => ({ message, path: path.join('.') }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return undefined;
|
|
22
|
+
};
|