@journeyapps-labs/reactor-mod-data-browser 2.0.2 → 2.2.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/@types/core/SchemaModelDefinition.d.ts +15 -1
  3. package/dist/@types/core/SchemaModelObject.d.ts +5 -0
  4. package/dist/@types/core/query/Page.d.ts +2 -0
  5. package/dist/@types/core/query/SimpleQuery.d.ts +4 -8
  6. package/dist/@types/core/query/filters.d.ts +17 -0
  7. package/dist/@types/core/query/widgets/BelongsToDisplayWidget.d.ts +14 -0
  8. package/dist/@types/core/query/widgets/CellDisplayWidget.d.ts +9 -0
  9. package/dist/@types/core/query/widgets/ColumnDisplayWidget.d.ts +5 -0
  10. package/dist/@types/core/query/widgets/SmartColumnWidget.d.ts +9 -0
  11. package/dist/@types/core/query/widgets/SmartFilterWidget.d.ts +9 -0
  12. package/dist/@types/forms/inputs/LocationInput.d.ts +13 -0
  13. package/dist/DataBrowserModule.js +1 -1
  14. package/dist/DataBrowserModule.js.map +1 -1
  15. package/dist/core/SchemaModelDefinition.js +56 -2
  16. package/dist/core/SchemaModelDefinition.js.map +1 -1
  17. package/dist/core/SchemaModelObject.js +72 -11
  18. package/dist/core/SchemaModelObject.js.map +1 -1
  19. package/dist/core/query/Page.js +5 -1
  20. package/dist/core/query/Page.js.map +1 -1
  21. package/dist/core/query/SimpleQuery.js +49 -83
  22. package/dist/core/query/SimpleQuery.js.map +1 -1
  23. package/dist/core/query/filters.js +17 -0
  24. package/dist/core/query/filters.js.map +1 -0
  25. package/dist/core/query/widgets/BelongsToDisplayWidget.js +70 -0
  26. package/dist/core/query/widgets/BelongsToDisplayWidget.js.map +1 -0
  27. package/dist/core/query/widgets/CellDisplayWidget.js +92 -0
  28. package/dist/core/query/widgets/CellDisplayWidget.js.map +1 -0
  29. package/dist/core/query/widgets/ColumnDisplayWidget.js +22 -0
  30. package/dist/core/query/widgets/ColumnDisplayWidget.js.map +1 -0
  31. package/dist/core/query/widgets/SmartColumnWidget.js +19 -0
  32. package/dist/core/query/widgets/SmartColumnWidget.js.map +1 -0
  33. package/dist/core/query/widgets/SmartFilterWidget.js +56 -0
  34. package/dist/core/query/widgets/SmartFilterWidget.js.map +1 -0
  35. package/dist/forms/SchemaModelForm.js +26 -11
  36. package/dist/forms/SchemaModelForm.js.map +1 -1
  37. package/dist/forms/inputs/LocationInput.js +74 -0
  38. package/dist/forms/inputs/LocationInput.js.map +1 -0
  39. package/dist/panels/model/ModelPanelFactory.js +1 -1
  40. package/dist/panels/model/ModelPanelFactory.js.map +1 -1
  41. package/dist/panels/model/ModelPanelWidget.js +21 -3
  42. package/dist/panels/model/ModelPanelWidget.js.map +1 -1
  43. package/dist/panels/query/QueryPanelWidget.js +6 -3
  44. package/dist/panels/query/QueryPanelWidget.js.map +1 -1
  45. package/dist/panels/query/TableControlsWidget.js +3 -3
  46. package/dist/panels/query/TableControlsWidget.js.map +1 -1
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/dist-module/bundle.js +44 -7
  49. package/dist-module/bundle.js.map +1 -1
  50. package/package.json +7 -5
  51. package/src/DataBrowserModule.ts +1 -1
  52. package/src/core/SchemaModelDefinition.ts +70 -3
  53. package/src/core/SchemaModelObject.ts +52 -2
  54. package/src/core/query/Page.ts +9 -1
  55. package/src/core/query/SimpleQuery.tsx +62 -111
  56. package/src/core/query/filters.ts +30 -0
  57. package/src/core/query/widgets/BelongsToDisplayWidget.tsx +107 -0
  58. package/src/core/query/widgets/CellDisplayWidget.tsx +122 -0
  59. package/src/core/query/widgets/ColumnDisplayWidget.tsx +27 -0
  60. package/src/core/query/widgets/SmartColumnWidget.tsx +30 -0
  61. package/src/core/query/widgets/SmartFilterWidget.tsx +80 -0
  62. package/src/forms/SchemaModelForm.tsx +28 -9
  63. package/src/forms/inputs/LocationInput.tsx +104 -0
  64. package/src/panels/model/ModelPanelFactory.tsx +1 -1
  65. package/src/panels/model/ModelPanelWidget.tsx +37 -3
  66. package/src/panels/query/QueryPanelWidget.tsx +6 -3
  67. package/src/panels/query/TableControlsWidget.tsx +4 -2
@@ -0,0 +1,122 @@
1
+ import {
2
+ CheckboxWidget,
3
+ ImageMedia,
4
+ MetadataWidget,
5
+ SmartDateDisplayWidget,
6
+ styled
7
+ } from '@journeyapps-labs/reactor-mod';
8
+ import { Attachment, Day, Location, Variable } from '@journeyapps/db';
9
+ import * as _ from 'lodash';
10
+ import * as React from 'react';
11
+ import { PageRow } from '../Page';
12
+
13
+ namespace S {
14
+ export const Empty = styled.div`
15
+ opacity: 0.2;
16
+ `;
17
+
18
+ export const Preview = styled.img`
19
+ max-height: 40px;
20
+ max-width: 40px;
21
+ cursor: pointer;
22
+ `;
23
+
24
+ export const pill = styled.div`
25
+ padding: 2px 4px;
26
+ background: ${(p) => p.theme.table.pills};
27
+ border-radius: 3px;
28
+ font-size: 12px;
29
+ `;
30
+
31
+ export const Pills = styled.div`
32
+ display: flex;
33
+ column-gap: 2px;
34
+ row-gap: 2px;
35
+ `;
36
+
37
+ export const Max = styled.div`
38
+ max-width: 500px;
39
+ white-space: pre;
40
+ display: inline;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ `;
44
+ }
45
+
46
+ export interface CellDisplayWidgetProps {
47
+ row: PageRow;
48
+ cell: any;
49
+ variable: Variable;
50
+ }
51
+
52
+ const MAX_NUMBER_OF_ARR_ITEMS_TO_DISPLAY = 3;
53
+
54
+ export const CellDisplayWidget: React.FC<CellDisplayWidgetProps> = (props) => {
55
+ const { row, cell, variable } = props;
56
+ if (cell == null) {
57
+ return <S.Empty>null</S.Empty>;
58
+ }
59
+ if (_.isString(cell)) {
60
+ if (cell.trim() === '') {
61
+ return <S.Empty>empty</S.Empty>;
62
+ }
63
+ return <S.Max>{cell}</S.Max>;
64
+ }
65
+ if (_.isNumber(cell)) {
66
+ return cell;
67
+ }
68
+ if (_.isArray(cell)) {
69
+ if (cell.length === 0) {
70
+ return <S.Empty>empty array</S.Empty>;
71
+ }
72
+
73
+ let items = _.slice(cell, 0, MAX_NUMBER_OF_ARR_ITEMS_TO_DISPLAY);
74
+
75
+ return (
76
+ <S.Pills>
77
+ {items.map((c) => {
78
+ return <S.pill key={c}>{c}</S.pill>;
79
+ })}
80
+ {items.length !== cell.length ? '...' : null}
81
+ </S.Pills>
82
+ );
83
+ }
84
+ if (cell instanceof Date) {
85
+ return <SmartDateDisplayWidget date={cell} />;
86
+ }
87
+ if (cell instanceof Day) {
88
+ return <SmartDateDisplayWidget date={cell.toDate()} />;
89
+ }
90
+ if (_.isBoolean(cell)) {
91
+ return <CheckboxWidget checked={cell} onChange={() => {}} />;
92
+ }
93
+ if (cell instanceof Location) {
94
+ return (
95
+ <>
96
+ <MetadataWidget label={'Lat'} value={`${cell.latitude}`} />
97
+ <MetadataWidget label={'Long'} value={`${cell.longitude}`} />
98
+ </>
99
+ );
100
+ }
101
+ if (cell instanceof Attachment) {
102
+ if (cell.uploaded()) {
103
+ return (
104
+ <S.Preview
105
+ onClick={() => {
106
+ row.model.getMedia(variable.name).then((media) => {
107
+ if (media instanceof ImageMedia) {
108
+ media.open();
109
+ } else {
110
+ window.open(cell.url(), '_blank');
111
+ }
112
+ });
113
+ }}
114
+ src={cell.urls['thumbnail']}
115
+ />
116
+ );
117
+ }
118
+ return <S.Empty>Not uploaded</S.Empty>;
119
+ }
120
+ console.log('unknown type', cell);
121
+ return null;
122
+ };
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+ import styled from '@emotion/styled';
3
+
4
+ export interface ColumnDisplayWidgetProps {
5
+ label: string;
6
+ }
7
+
8
+ export const ColumnDisplayWidget: React.FC<ColumnDisplayWidgetProps> = (props) => {
9
+ let parts = (props.label || '').split(' ');
10
+ if (parts.length >= 5) {
11
+ return <S.Width length={150}>{props.label}</S.Width>;
12
+ }
13
+ if (parts.length >= 3) {
14
+ return <S.Width length={80}>{props.label}</S.Width>;
15
+ }
16
+ return <S.Span>{props.label}</S.Span>;
17
+ };
18
+
19
+ namespace S {
20
+ export const Width = styled.div<{ length: number }>`
21
+ min-width: ${(p) => p.length}px;
22
+ `;
23
+
24
+ export const Span = styled.div`
25
+ white-space: nowrap;
26
+ `;
27
+ }
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ import styled from '@emotion/styled';
3
+ import { Variable } from '@journeyapps/db';
4
+ import { ColumnDisplayWidget } from './ColumnDisplayWidget';
5
+ import { SmartFilterWidget } from './SmartFilterWidget';
6
+ import { SimpleFilter } from '../filters';
7
+
8
+ export interface SmartColumnWidgetProps {
9
+ variable: Variable;
10
+ filter?: SimpleFilter;
11
+ filterChanged: (filter: SimpleFilter | null) => any;
12
+ }
13
+
14
+ export const SmartColumnWidget: React.FC<SmartColumnWidgetProps> = (props) => {
15
+ return (
16
+ <S.Container>
17
+ <ColumnDisplayWidget label={props.variable.label} />
18
+ <SmartFilterWidget filter={props.filter} variable={props.variable} filterChanged={props.filterChanged} />
19
+ </S.Container>
20
+ );
21
+ };
22
+
23
+ namespace S {
24
+ export const Container = styled.div`
25
+ display: flex;
26
+ flex-direction: row;
27
+ align-items: center;
28
+ column-gap: 5px;
29
+ `;
30
+ }
@@ -0,0 +1,80 @@
1
+ import * as React from 'react';
2
+ import { SingleChoiceIntegerType, SingleChoiceType, TextType, Variable } from '@journeyapps/db';
3
+ import { ComboBoxStore, DialogStore, ioc, PanelButtonWidget } from '@journeyapps-labs/reactor-mod';
4
+ import * as _ from 'lodash';
5
+ import { Condition, SimpleFilter } from '../filters';
6
+
7
+ export interface SmartFilterWidgetProps {
8
+ variable: Variable;
9
+ filter?: SimpleFilter;
10
+ filterChanged: (filter: SimpleFilter | null) => any;
11
+ }
12
+
13
+ export const SmartFilterWidget: React.FC<SmartFilterWidgetProps> = (props) => {
14
+ if (props.variable.type instanceof SingleChoiceIntegerType || props.variable.type instanceof SingleChoiceType) {
15
+ return (
16
+ <PanelButtonWidget
17
+ {...{
18
+ icon: 'filter',
19
+ highlight: !!props.filter,
20
+ action: async (position) => {
21
+ let results = await ioc.get(ComboBoxStore).showMultiSelectComboBox(
22
+ _.map(props.variable.type.options, (option) => {
23
+ return {
24
+ title: `${option.value}`,
25
+ key: `${option.value}`,
26
+ checked: !!props.filter?.statements?.find((s) => s.arg === `${option.value}`)
27
+ };
28
+ }),
29
+ position
30
+ );
31
+ if (results.length > 0) {
32
+ props.filterChanged(
33
+ new SimpleFilter(
34
+ props.variable,
35
+ results.map((r) => {
36
+ return {
37
+ arg: r.key,
38
+ condition: Condition.EQUALS
39
+ };
40
+ })
41
+ )
42
+ );
43
+ } else {
44
+ props.filterChanged(null);
45
+ }
46
+ }
47
+ }}
48
+ />
49
+ );
50
+ }
51
+ if (props.variable.type instanceof TextType) {
52
+ return (
53
+ <PanelButtonWidget
54
+ {...{
55
+ icon: 'filter',
56
+ highlight: !!props.filter,
57
+ action: async () => {
58
+ let value = await ioc.get(DialogStore).showInputDialog({
59
+ title: `${props.variable.label}`,
60
+ initialValue: props.filter?.statements[0]?.arg
61
+ });
62
+ if (value) {
63
+ props.filterChanged(
64
+ new SimpleFilter(props.variable, [
65
+ {
66
+ arg: value,
67
+ condition: Condition.EQUALS
68
+ }
69
+ ])
70
+ );
71
+ } else {
72
+ props.filterChanged(null);
73
+ }
74
+ }
75
+ }}
76
+ />
77
+ );
78
+ }
79
+ return null;
80
+ };
@@ -2,7 +2,10 @@ import {
2
2
  BooleanInput,
3
3
  DateInput,
4
4
  DateTimePickerType,
5
+ FileInput,
5
6
  FormModel,
7
+ ImageInput,
8
+ ImageMedia,
6
9
  SelectInput,
7
10
  TextInput
8
11
  } from '@journeyapps-labs/reactor-mod';
@@ -22,6 +25,7 @@ import {
22
25
  SingleChoiceType,
23
26
  TextType
24
27
  } from '@journeyapps/db';
28
+ import { LocationInput } from './inputs/LocationInput';
25
29
 
26
30
  export interface SchemaModelFormOptions {
27
31
  definition: SchemaModelDefinition;
@@ -49,13 +53,25 @@ export class SchemaModelForm extends FormModel {
49
53
  type: DateTimePickerType.DATE
50
54
  });
51
55
  }
52
- if (attribute.type instanceof AttachmentType) {
53
- }
54
- if (attribute.type instanceof SignatureType) {
55
- console.log(options.object?.model[attribute.name]);
56
+ if (attribute.type instanceof SignatureType || attribute.type instanceof PhotoType) {
57
+ let media = new ImageInput({
58
+ name: attribute.name,
59
+ label: attribute.label
60
+ });
61
+
62
+ if (options.object) {
63
+ options.object.getMedia(attribute.name).then((m) => {
64
+ media.setValue(m as ImageMedia);
65
+ });
66
+ }
67
+ return media;
56
68
  }
57
- if (attribute.type instanceof PhotoType) {
58
- console.log(options.object?.model[attribute.name]);
69
+ if (attribute.type instanceof AttachmentType) {
70
+ return new FileInput({
71
+ name: attribute.name,
72
+ label: attribute.label,
73
+ value: options.object?.model[attribute.name] || null
74
+ });
59
75
  }
60
76
  if (attribute.type instanceof BooleanType) {
61
77
  return new BooleanInput({
@@ -65,10 +81,13 @@ export class SchemaModelForm extends FormModel {
65
81
  });
66
82
  }
67
83
  if (attribute.type instanceof LocationType) {
84
+ return new LocationInput({
85
+ name: attribute.name,
86
+ label: attribute.label,
87
+ value: options.object?.model[attribute.name]
88
+ });
68
89
  }
69
- if (attribute.type instanceof SingleChoiceIntegerType) {
70
- }
71
- if (attribute.type instanceof SingleChoiceType) {
90
+ if (attribute.type instanceof SingleChoiceIntegerType || attribute.type instanceof SingleChoiceType) {
72
91
  return new SelectInput({
73
92
  name: attribute.name,
74
93
  label: attribute.label,
@@ -0,0 +1,104 @@
1
+ import {
2
+ FormInput,
3
+ FormInputGenerics,
4
+ FormInputOptions,
5
+ FormInputRenderOptions,
6
+ InputContainerWidget,
7
+ NumberInput,
8
+ PanelButtonMode,
9
+ PanelButtonWidget
10
+ } from '@journeyapps-labs/reactor-mod';
11
+ import * as React from 'react';
12
+ import { Location } from '@journeyapps/db';
13
+ import styled from '@emotion/styled';
14
+
15
+ export class LocationInput extends FormInput<FormInputGenerics & { VALUE: Location }> {
16
+ latitude: NumberInput;
17
+ longitude: NumberInput;
18
+
19
+ constructor(options: FormInputOptions<Location>) {
20
+ super(options);
21
+ this.latitude = new NumberInput({
22
+ name: 'latitude',
23
+ label: 'Latitude',
24
+ value: options.value?.latitude
25
+ });
26
+ this.longitude = new NumberInput({
27
+ name: 'longitude',
28
+ label: 'Longitude',
29
+ value: options.value?.longitude
30
+ });
31
+
32
+ this.update();
33
+
34
+ [this.latitude, this.longitude].forEach((l) => {
35
+ l.registerListener({
36
+ valueChanged: () => {
37
+ this.update();
38
+ }
39
+ });
40
+ });
41
+ }
42
+
43
+ setValue(value: Location) {
44
+ super.setValue(value);
45
+ if (this.latitude.value !== value?.latitude) {
46
+ this.latitude.setValue(value?.latitude);
47
+ }
48
+ if (this.longitude.value !== value?.longitude) {
49
+ this.longitude.setValue(value?.longitude);
50
+ }
51
+ }
52
+
53
+ update() {
54
+ let lat = this.latitude.value;
55
+ let long = this.longitude.value;
56
+ if (lat == null || long === null) {
57
+ this.setValue(null);
58
+ } else {
59
+ this.setValue(
60
+ new Location({
61
+ latitude: lat,
62
+ longitude: long,
63
+ horizontal_accuracy: (this.value as Location)?.horizontal_accuracy,
64
+ vertical_accuracy: (this.value as Location)?.vertical_accuracy,
65
+ altitude: (this.value as Location)?.altitude,
66
+ timestamp: new Date()
67
+ })
68
+ );
69
+ }
70
+ }
71
+
72
+ renderControl(options: FormInputRenderOptions): React.JSX.Element {
73
+ return (
74
+ <S.Container>
75
+ <InputContainerWidget error={this.latitude.error} label={this.latitude.label} inline={true}>
76
+ {this.latitude.renderControl({ inline: true })}
77
+ </InputContainerWidget>
78
+ <InputContainerWidget error={this.longitude.error} label={this.longitude.label} inline={true}>
79
+ {this.longitude.renderControl({ inline: true })}
80
+ </InputContainerWidget>
81
+ {this.value ? (
82
+ <PanelButtonWidget
83
+ mode={PanelButtonMode.LINK}
84
+ label="Open in maps"
85
+ icon="map"
86
+ action={() => {
87
+ window.open(`https://www.google.com/maps/?q=${this.latitude.value},${this.longitude.value}`, '_blank');
88
+ }}
89
+ />
90
+ ) : null}
91
+ </S.Container>
92
+ );
93
+ }
94
+ }
95
+
96
+ namespace S {
97
+ export const Container = styled.div`
98
+ display: flex;
99
+ flex-direction: column;
100
+ row-gap: 2px;
101
+ max-width: 250px;
102
+ align-items: flex-start;
103
+ `;
104
+ }
@@ -24,7 +24,7 @@ export class ModelPanelModel extends ReactorPanelModel {
24
24
 
25
25
  constructor(options?: ModelPanelModelOptions) {
26
26
  super(ModelPanelFactory.TYPE);
27
- this.setExpand(true, true);
27
+ this.setExpand(false, true);
28
28
  this.definition = options?.definition;
29
29
  this.model = options?.model;
30
30
  }
@@ -2,7 +2,13 @@ import * as React from 'react';
2
2
  import { useEffect, useState } from 'react';
3
3
  import { observer } from 'mobx-react';
4
4
  import styled from '@emotion/styled';
5
- import { BorderLayoutWidget, LoadingPanelWidget } from '@journeyapps-labs/reactor-mod';
5
+ import {
6
+ BorderLayoutWidget,
7
+ LoadingPanelWidget,
8
+ PANEL_CONTENT_PADDING,
9
+ PanelToolbarWidget,
10
+ ScrollableDivCss
11
+ } from '@journeyapps-labs/reactor-mod';
6
12
 
7
13
  import { SchemaModelForm } from '../../forms/SchemaModelForm';
8
14
  import { ModelPanelModel } from './ModelPanelFactory';
@@ -13,7 +19,9 @@ export interface QueryPanelWidgetProps {
13
19
 
14
20
  namespace S {
15
21
  export const Container = styled.div`
16
- padding: 20px;
22
+ overflow: auto;
23
+ padding: ${PANEL_CONTENT_PADDING}px;
24
+ ${ScrollableDivCss};
17
25
  `;
18
26
  }
19
27
 
@@ -32,10 +40,36 @@ export const ModelPanelWidget: React.FC<QueryPanelWidgetProps> = observer((props
32
40
  );
33
41
  }, [props.model.model, props.model.definition]);
34
42
 
43
+ let top = null;
44
+ if (props.model.model) {
45
+ top = (
46
+ <PanelToolbarWidget
47
+ btns={
48
+ [
49
+ // {
50
+ // label: 'Delete object',
51
+ // action: () => {}
52
+ // }
53
+ ]
54
+ }
55
+ meta={[
56
+ {
57
+ label: 'ID',
58
+ value: props.model?.id
59
+ }
60
+ ]}
61
+ />
62
+ );
63
+ }
64
+
35
65
  return (
36
66
  <LoadingPanelWidget loading={!form}>
37
67
  {() => {
38
- return <S.Container>{form.render()}</S.Container>;
68
+ return (
69
+ <BorderLayoutWidget top={top}>
70
+ <S.Container>{form.render()}</S.Container>
71
+ </BorderLayoutWidget>
72
+ );
39
73
  }}
40
74
  </LoadingPanelWidget>
41
75
  );
@@ -7,6 +7,7 @@ import { BorderLayoutWidget, LoadingPanelWidget } from '@journeyapps-labs/reacto
7
7
  import { Page } from '../../core/query/Page';
8
8
  import { PageResultsWidget } from './PageResultsWidget';
9
9
  import { TableControlsWidget } from './TableControlsWidget';
10
+ import { autorun } from 'mobx';
10
11
 
11
12
  export interface QueryPanelWidgetProps {
12
13
  model: QueryPanelModel;
@@ -30,9 +31,11 @@ export const QueryPanelWidget: React.FC<QueryPanelWidgetProps> = observer((props
30
31
  }, [props.model.query]);
31
32
 
32
33
  useEffect(() => {
33
- if (props.model.query) {
34
- setPage(props.model.query.getPage(props.model.current_page));
35
- }
34
+ return autorun(() => {
35
+ if (props.model.query) {
36
+ setPage(props.model.query.getPage(props.model.current_page));
37
+ }
38
+ });
36
39
  }, [props.model.query]);
37
40
 
38
41
  return (
@@ -46,6 +46,7 @@ export const TableControlsWidget: React.FC<TableControlsWidgetProps> = observer(
46
46
  />
47
47
  <PanelButtonWidget
48
48
  label="Page"
49
+ tooltip="Reload page"
49
50
  icon="refresh"
50
51
  action={() => {
51
52
  props.current_page.load();
@@ -53,9 +54,10 @@ export const TableControlsWidget: React.FC<TableControlsWidgetProps> = observer(
53
54
  />
54
55
  <PanelButtonWidget
55
56
  label="Query"
57
+ tooltip="Reload Query"
56
58
  icon="refresh"
57
- action={() => {
58
- props.query.load();
59
+ action={async (event, loading) => {
60
+ await props.query.load();
59
61
  }}
60
62
  />
61
63
  </S.Container>