mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
d5a4605d2a
Summary: - Using a sample of data was causing poor detection if the sample were cut mid-character. Switch to using line-based detection. - Add a simple option for changing encoding. No convenient UI is offered since config UI is auto-generated, but this at least makes it possible to recover from bad guesses. - Upgrades chardet library for good measure. - Also fixes python3-building step, to more reliably rebuild Python dependencies when requirements3.* files change. Test Plan: Added a python-side test case, and a browser test that encodings can be switched, errors are displayed, and wrong encodings fail recoverably. Reviewers: alexmojaki Reviewed By: alexmojaki Differential Revision: https://phab.getgrist.com/D3979
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
|
import {squareCheckbox} from 'app/client/ui2018/checkbox';
|
|
import {testId, theme} from 'app/client/ui2018/cssVars';
|
|
import {makeLinks} from 'app/client/ui2018/links';
|
|
import {cssModalButtons} from 'app/client/ui2018/modals';
|
|
import {ParseOptionSchema} from 'app/plugin/FileParserAPI';
|
|
import {Computed, dom, DomContents, IDisposableOwner, input, Observable, styled} from 'grainjs';
|
|
import fromPairs = require('lodash/fromPairs');
|
|
import invert = require('lodash/invert');
|
|
|
|
export type ParseOptionValueType = boolean|string|number;
|
|
|
|
export interface ParseOptionValues {
|
|
[name: string]: ParseOptionValueType;
|
|
}
|
|
|
|
/**
|
|
* EscapeChars contains mapping for some escape characters that we need to convert
|
|
* for displaying in input fields
|
|
*/
|
|
interface EscapeChars {
|
|
[char: string]: string;
|
|
}
|
|
|
|
const escapeCharDict: EscapeChars = {
|
|
'\n': '\\n',
|
|
'\r': '\\r',
|
|
'\t': '\\t',
|
|
};
|
|
const invertedEscapeCharDict: EscapeChars = invert(escapeCharDict);
|
|
|
|
// Helpers to escape and unescape certain non-printable characters that are useful in parsing
|
|
// options, e.g. as separators.
|
|
function escapeChars(value: string) {
|
|
return value.replace(/[\n\r\t]/g, (match) => escapeCharDict[match]);
|
|
}
|
|
function unescapeChars(value: string) {
|
|
return value.replace(/\\[nrt]/g, (match) => invertedEscapeCharDict[match]);
|
|
}
|
|
|
|
/**
|
|
* Builds a DOM form consisting of inputs built according to schema, with the passed-in values.
|
|
* The included "Update" button is enabled if any value has changed, and calls doUpdate() with the
|
|
* current values.
|
|
*/
|
|
export function buildParseOptionsForm(
|
|
owner: IDisposableOwner,
|
|
schema: ParseOptionSchema[],
|
|
values: ParseOptionValues,
|
|
doUpdate: (v: ParseOptionValues) => void,
|
|
doCancel: () => void,
|
|
): DomContents {
|
|
const items = schema.filter(item => item.visible);
|
|
const optionsMap = new Map<string, Observable<ParseOptionValueType>>(
|
|
items.map((item) => [item.name, Observable.create(owner, values[item.name])]));
|
|
|
|
function collectParseOptions(): ParseOptionValues {
|
|
return fromPairs(items.map((item) => [item.name, optionsMap.get(item.name)!.get()]));
|
|
}
|
|
|
|
return [
|
|
cssParseOptionForm(
|
|
items.map((item) => cssParseOption(
|
|
cssParseOptionName(makeLinks(item.label)),
|
|
optionToInput(owner, item.type, optionsMap.get(item.name)!),
|
|
testId('parseopts-opt'),
|
|
)),
|
|
),
|
|
cssModalButtons(
|
|
dom.domComputed((use) => items.every((item) => use(optionsMap.get(item.name)!) === values[item.name]),
|
|
(unchanged) => (unchanged ?
|
|
bigBasicButton('Close', dom.on('click', doCancel), testId('parseopts-back')) :
|
|
bigPrimaryButton('Update preview', dom.on('click', () => doUpdate(collectParseOptions())),
|
|
testId('parseopts-update'))
|
|
)
|
|
)
|
|
),
|
|
];
|
|
}
|
|
|
|
function optionToInput(owner: IDisposableOwner, type: string, value: Observable<ParseOptionValueType>): HTMLElement {
|
|
switch (type) {
|
|
case 'boolean': return squareCheckbox(value as Observable<boolean>);
|
|
default: {
|
|
const obs = Computed.create(owner, (use) => escapeChars(String(use(value) || "")))
|
|
.onWrite((val) => value.set(unescapeChars(val)));
|
|
return cssInputText(obs, {onInput: true},
|
|
dom.on('focus', (ev, elem) => elem.select()));
|
|
}
|
|
}
|
|
}
|
|
|
|
const cssParseOptionForm = styled('div', `
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: space-between;
|
|
padding: 16px 0;
|
|
width: 400px;
|
|
overflow-y: auto;
|
|
`);
|
|
const cssParseOption = styled('div', `
|
|
flex: none;
|
|
margin: 8px 0;
|
|
width: calc(50% - 16px);
|
|
font-weight: initial; /* negate bootstrap */
|
|
`);
|
|
const cssParseOptionName = styled('div', `
|
|
margin-bottom: 8px;
|
|
`);
|
|
const cssInputText = styled(input, `
|
|
color: ${theme.inputFg};
|
|
background-color: ${theme.inputBg};
|
|
position: relative;
|
|
display: inline-block;
|
|
outline: none;
|
|
height: 28px;
|
|
border: 1px solid ${theme.inputBorder};
|
|
border-radius: 3px;
|
|
padding: 0 6px;
|
|
width: 100%;
|
|
|
|
&::placeholder {
|
|
color: ${theme.inputPlaceholderFg};
|
|
}
|
|
`);
|