From 6dedde6f391b9b689271a86ae5d02c2cbc37944d Mon Sep 17 00:00:00 2001 From: roll Date: Mon, 14 Jun 2021 17:39:05 +0200 Subject: [PATCH 01/47] Added Schema component (#14) * Bootstrapped Schema * Added schema styles * Added schema dependencies * Added SchemaFeedback * Added SchemaPreview * Added SchemaField * Added Schema * Added schema helpers * Ignored fs module * Fixed schema editor error * Fixed Field update/remove * Fixed the preview * Fixed the Details button * Fixed styles * Fixed sorting * Removed debug * Fixed visual sorting * Added a story with source --- .eslintrc | 1 + .storybook/main.js | 5 +- docs/Schema.stories.tsx | 24 ++ package.json | 6 +- src/components/Schema.tsx | 234 ++++++++++ src/components/SchemaFeedback.tsx | 37 ++ src/components/SchemaField.tsx | 193 +++++++++ src/components/SchemaPreview.tsx | 20 + src/components/index.ts | 3 + src/config.ts | 25 ++ src/entry.ts | 4 +- src/helpers.ts | 110 +++++ src/index.ts | 6 +- src/styles/index.ts | 1 + src/styles/schema.css | 683 ++++++++++++++++++++++++++++++ webpack.config.js | 3 + 16 files changed, 1347 insertions(+), 8 deletions(-) create mode 100644 docs/Schema.stories.tsx create mode 100644 src/components/Schema.tsx create mode 100644 src/components/SchemaFeedback.tsx create mode 100644 src/components/SchemaField.tsx create mode 100644 src/components/SchemaPreview.tsx create mode 100644 src/components/index.ts create mode 100644 src/config.ts create mode 100644 src/styles/schema.css diff --git a/.eslintrc b/.eslintrc index 27f11e2..b19ef59 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,6 +21,7 @@ "rules": { "indent": "off", "no-unused-vars": "off", + "multiline-ternary": "off", "import/no-duplicates": "off", "no-use-before-define": "off", "no-useless-constructor": "off", diff --git a/.storybook/main.js b/.storybook/main.js index 3b66790..2b96ed8 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,5 +1,8 @@ module.exports = { stories: ['../docs/**/*.stories.mdx', '../docs/**/*.stories.@(js|jsx|ts|tsx)'], addons: ['@storybook/addon-links', '@storybook/addon-essentials'], + webpackFinal: async (config, { configType }) => { + config.node = { ...config.node, fs: 'empty' } + return config + }, } - diff --git a/docs/Schema.stories.tsx b/docs/Schema.stories.tsx new file mode 100644 index 0000000..2f9eabb --- /dev/null +++ b/docs/Schema.stories.tsx @@ -0,0 +1,24 @@ +import '../src/styles' +import React from 'react' +import { Story, Meta } from '@storybook/react' +import { Schema, ISchemaProps } from '../src' + +export default { + title: 'Components/Schema', + component: Schema, +} as Meta + +const Template: Story = (args) => +const onSave = () => alert('Clicked on the "Save button"') + +export const Empty = Template.bind({}) +Empty.args = { + onSave, +} + +export const Source = Template.bind({}) +Source.args = { + source: + 'https://raw.githubusercontent.com/frictionlessdata/frictionless-py/main/data/table.csv', + onSave, +} diff --git a/package.json b/package.json index 8b48ad9..729898f 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,16 @@ "dependencies": { "@types/lodash": "^4.14.168", "@types/marked": "^2.0.1", + "@types/uuid": "^8.3.0", "classnames": "^2.3.1", "jsonschema": "^1.4.0", "jszip": "^3.6.0", "lodash": "^4.17.21", "marked": "^2.0.1", - "use-async-effect": "^2.2.3" + "react-sortable-hoc": "^2.0.0", + "tableschema": "^1.12.4", + "use-async-effect": "^2.2.3", + "uuid": "^8.3.2" }, "peerDependencies": { "react": "^17.0.2", diff --git a/src/components/Schema.tsx b/src/components/Schema.tsx new file mode 100644 index 0000000..24242d4 --- /dev/null +++ b/src/components/Schema.tsx @@ -0,0 +1,234 @@ +import { find } from 'lodash' +import classNames from 'classnames' +import React, { useState } from 'react' +import { arrayMove } from 'react-sortable-hoc' +import { useAsyncEffect } from 'use-async-effect' +import { SortableContainer, SortableElement } from 'react-sortable-hoc' +import { SchemaFeedback, ISchemaFeedbackProps } from './SchemaFeedback' +import { SchemaPreview } from './SchemaPreview' +import { SchemaField } from './SchemaField' +import * as helpers from '../helpers' +import { IDict } from '../common' + +export interface ISchemaProps { + source?: string | File + schema: IDict | File + onSave: any + disablePreview: boolean +} + +export function Schema(props: ISchemaProps) { + const [tab, setTab] = useState('edit' as 'edit' | 'preview') + const [error, setError] = useState(null as null | Error) + const [loading, setLoading] = useState(false) + const [columns, setColumns] = useState([] as IDict[]) + const [metadata, setMetadata] = useState({} as IDict) + const [feedback] = useState({} as ISchemaFeedbackProps['feedback']) + + // Mount + useAsyncEffect(async () => { + try { + setLoading(false) + const { columns, metadata } = await helpers.importSchema(props.source, props.schema) + setLoading(false) + setColumns(columns) + setMetadata(metadata) + } catch (error) { + setError(error) + setLoading(false) + } + }, []) + + // Save + const saveSchema = () => { + if (props.onSave) { + const schema = helpers.exportSchema(columns, metadata) + props.onSave(schema, error) + } + } + + // Feedback Reset + const resetFeedback = () => {} + + // Add Field + const addField = () => { + setColumns([...columns, helpers.createColumn(columns.length)]) + } + + // Remove Field + const removeField = (id: number): void => { + const newColumns = columns.filter((column) => column.id !== id) + setColumns([...newColumns]) + } + + // Update Field + const updateField = (id: number, name: string, value: any): void => { + const column = find(columns, (column) => column.id === id) + if (column) { + column.field[name] = value + setColumns([...columns]) + } + } + + // Move Field + const moveField = (props: { oldIndex: number; newIndex: number }) => { + setColumns([...arrayMove(columns, props.oldIndex, props.newIndex)]) + } + + return ( +
+
+ {/* Tab navigation */} + + +
+ + {/* Feedback */} + + + {/* Tab contents */} + {!loading && !error && ( +
+ {/* Edit */} + {tab === 'edit' && ( +
+
+ {/* List fields */} + + + {/* Add field */} +
+ +
+
+
+ )} + + {/* Preview */} + {tab === 'preview' && ( +
+ +
+ )} +
+ )} +
+
+ ) +} + +// Internal + +const SortableFields = SortableContainer( + (props: { columns: IDict[]; metadata: IDict; removeField: any; updateField: any }) => ( +
    + {props.columns.map((column: IDict, index: number) => ( + + ))} +
+ ) +) + +const SortableField = SortableElement( + (props: { column: IDict; metadata: IDict; removeField: any; updateField: any }) => ( +
  • + +
  • + ) +) diff --git a/src/components/SchemaFeedback.tsx b/src/components/SchemaFeedback.tsx new file mode 100644 index 0000000..3bb012c --- /dev/null +++ b/src/components/SchemaFeedback.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import classNames from 'classnames' + +export interface ISchemaFeedbackProps { + onReset: () => void + feedback: { + message: string + reset: boolean + type: string + } +} + +export function SchemaFeedback(props: ISchemaFeedbackProps) { + return ( +
    +
    + {props.feedback.message} + {!!props.feedback.reset && ( + +  To start from scratch please  + { + ev.preventDefault() + props.onReset() + }} + > + click here + + . + + )} +
    +
    + ) +} diff --git a/src/components/SchemaField.tsx b/src/components/SchemaField.tsx new file mode 100644 index 0000000..4213a5a --- /dev/null +++ b/src/components/SchemaField.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react' +import { IDict } from '../common' +import * as helpers from '../helpers' + +export interface ISchemaFieldProps { + column: IDict + metadata: IDict + removeField: any + updateField: any +} + +export function SchemaField(props: ISchemaFieldProps) { + const types = helpers.getFieldTypes() + const formats = helpers.getFieldFormats(props.column.field.type) + const [isDetails, setIsDetails] = useState(false) + return ( +
    + {/* General */} +
    + {/* Name */} +
    +
    +
    +
    +
    Name
    +
    + props.updateField(props.column.id, 'name', ev.target.value)} + /> +
    +
    + + {/* Type */} +
    +
    +
    +
    Type
    +
    + +
    +
    + + {/* Format */} +
    +
    +
    +
    Format
    +
    + { + props.updateField(props.column.id, 'format', ev.target.value) + }} + /> +
    +
    + + {/* Controls */} +
    + {/* Details */} + + + {/* Remove */} + +
    +
    + + {/* Details */} + {isDetails && ( +
    +
    +
    +
    + {/* Extra fields */} +
    + {/* Title */} +
    + + + props.updateField(props.column.id, 'title', ev.target.value) + } + /> +
    + + {/* Description */} +
    + +