Skip to content

Commit

Permalink
feat(admin-ui): Add json editor field input component
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Nov 26, 2021
1 parent 152e64b commit 4297b87
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 22 deletions.
1 change: 1 addition & 0 deletions packages/admin-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@webcomponents/custom-elements": "^1.4.3",
"apollo-angular": "^2.4.0",
"apollo-upload-client": "^14.1.3",
"codejar": "^3.5.0",
"core-js": "^3.9.1",
"dayjs": "^1.10.4",
"graphql": "15.5.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div #editor class="json-editor" [class.invalid]="!isValid" [style.height]="height || '300px'"></div>
<div class="error-message">
<span *ngIf="errorMessage">{{ errorMessage }}</span>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.json-editor {
min-height: 6rem;
background-color: var(--color-json-editor-background-color);
color: var(--color-json-editor-text);
border: 1px solid var(--color-component-border-200);
border-radius: 3px;
padding: 6px;
tab-size: 4;
font-family: 'Source Code Pro', 'Lucida Console', Monaco, monospace;
font-size: 14px;
font-weight: 400;
height: 340px;
letter-spacing: normal;
line-height: 20px;

&:focus {
border-color: var(--color-primary-500);
}

&.invalid {
border-color: var(--clr-forms-invalid-color);
}

// prettier-ignore
::ng-deep {
.je-string { color: var(--color-json-editor-string); }
.je-number { color: var(--color-json-editor-number); }
.je-boolean { color: var(--color-json-editor-boolean); }
.je-null { color: var(--color-json-editor-null); }
.je-key { color: var(--color-json-editor-key); }
.je-error {
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-color: var(--color-json-editor-error);
}
}
}

.error-message {
min-height: 1rem;
color: var(--color-json-editor-error);
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
OnInit,
ViewChild,
} from '@angular/core';
import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
import { CodeJar } from 'codejar';

import { FormInputComponent } from '../../../common/component-registry-types';

export function jsonValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const error: ValidationErrors = { jsonInvalid: true };

try {
JSON.parse(control.value);
} catch (e) {
control.setErrors(error);
return error;
}

control.setErrors(null);
return null;
};
}

@Component({
selector: 'vdr-json-editor-form-input',
templateUrl: './json-editor-form-input.component.html',
styleUrls: ['./json-editor-form-input.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class JsonEditorFormInputComponent implements FormInputComponent, AfterViewInit, OnInit {
static readonly id: DefaultFormComponentId = 'json-editor-form-input';
readonly: boolean;
formControl: FormControl;
config: DefaultFormComponentConfig<'json-editor-form-input'>;
isValid = true;
height: DefaultFormComponentConfig<'json-editor-form-input'>['height'];
errorMessage: string | undefined;
@ViewChild('editor') private editorElementRef: ElementRef<HTMLDivElement>;
jar: CodeJar;

constructor(private changeDetector: ChangeDetectorRef) {}

ngOnInit() {
this.formControl.addValidators(jsonValidator());
}

ngAfterViewInit() {
let lastVal = '';
const highlight = (editor: HTMLElement) => {
const code = editor.textContent ?? '';
if (code === lastVal) {
return;
}
lastVal = code;
this.errorMessage = this.getJsonError(code);
this.changeDetector.markForCheck();
editor.innerHTML = this.syntaxHighlight(code, this.getErrorPos(this.errorMessage));
};
this.jar = CodeJar(this.editorElementRef.nativeElement, highlight);
this.jar.onUpdate(value => {
this.formControl.setValue(value);
this.formControl.markAsDirty();
this.isValid = this.formControl.valid;
});
this.jar.updateCode(this.formControl.value);

if (this.readonly) {
this.editorElementRef.nativeElement.contentEditable = 'false';
}
}

private getJsonError(json: string): string | undefined {
try {
JSON.parse(json);
} catch (e) {
return e.message;
}
return;
}

private getErrorPos(errorMessage: string | undefined): number | undefined {
if (!errorMessage) {
return;
}
const matches = errorMessage.match(/at position (\d+)/);
const pos = matches?.[1];
return pos != null ? +pos : undefined;
}

private syntaxHighlight(json: string, errorPos: number | undefined) {
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
let hasMarkedError = false;
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
(match, ...args) => {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
let errorClass = '';
if (errorPos && !hasMarkedError) {
const length = args[0].length;
const offset = args[4];
if (errorPos <= length + offset) {
errorClass = 'je-error';
hasMarkedError = true;
}
}
return '<span class="je-' + cls + ' ' + errorClass + '">' + match + '</span>';
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../../providers/custom-field-component/custom-field-component.service';

import { BooleanFormInputComponent } from './boolean-form-input/boolean-form-input.component';
import { JsonEditorFormInputComponent } from './code-editor-form-input/json-editor-form-input.component';
import { CurrencyFormInputComponent } from './currency-form-input/currency-form-input.component';
import { CustomerGroupFormInputComponent } from './customer-group-form-input/customer-group-form-input.component';
import { DateFormInputComponent } from './date-form-input/date-form-input.component';
Expand Down Expand Up @@ -36,6 +37,7 @@ export const defaultFormInputs = [
RelationFormInputComponent,
TextareaFormInputComponent,
RichTextFormInputComponent,
JsonEditorFormInputComponent,
];

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/admin-ui/src/lib/core/src/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { IfDefaultChannelActiveDirective } from './directives/if-default-channel
import { IfMultichannelDirective } from './directives/if-multichannel.directive';
import { IfPermissionsDirective } from './directives/if-permissions.directive';
import { BooleanFormInputComponent } from './dynamic-form-inputs/boolean-form-input/boolean-form-input.component';
import { JsonEditorFormInputComponent } from './dynamic-form-inputs/code-editor-form-input/json-editor-form-input.component';
import { CurrencyFormInputComponent } from './dynamic-form-inputs/currency-form-input/currency-form-input.component';
import { CustomerGroupFormInputComponent } from './dynamic-form-inputs/customer-group-form-input/customer-group-form-input.component';
import { DateFormInputComponent } from './dynamic-form-inputs/date-form-input/date-form-input.component';
Expand Down Expand Up @@ -244,6 +245,7 @@ const DYNAMIC_FORM_INPUTS = [
RelationSelectorDialogComponent,
TextareaFormInputComponent,
RichTextFormInputComponent,
JsonEditorFormInputComponent,
];

@NgModule({
Expand Down
9 changes: 9 additions & 0 deletions packages/admin-ui/src/lib/static/styles/theme/dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@
--color-form-input-bg: hsl(212, 35%, 95%);
--color-timeline-thread: var(--color-primary-700);

--color-json-editor-background-color: var(--color-grey-600);
--color-json-editor-text: var(--color-grey-100);
--color-json-editor-string: var(--color-secondary-300);
--color-json-editor-number: var(--color-primary-300);
--color-json-editor-boolean: var(--color-primary-300);
--color-json-editor-null: var(--color-grey-300-grey-500);
--color-json-editor-key: var(--color-success-300);
--color-json-editor-error: var(--color-error-200);

// clarity styles
--clr-global-app-background: hsl(201, 30%, 15%);
--clr-global-selection-color: hsl(203, 32%, 29%);
Expand Down
8 changes: 8 additions & 0 deletions packages/admin-ui/src/lib/static/styles/theme/default.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@
--color-chip-error-border: var(--color-error-200);
--color-chip-error-text: var(--color-error-600);
--color-chip-error-bg: var(--color-error-100);
--color-json-editor-background-color: var(--color-grey-200);
--color-json-editor-text: var(--color-grey-600);
--color-json-editor-string: var(--color-secondary-600);
--color-json-editor-number: var(--color-primary-600);
--color-json-editor-boolean: var(--color-primary-600);
--color-json-editor-null: var(--color-grey-500);
--color-json-editor-key: var(--color-success-500);
--color-json-editor-error: var(--color-error-500);

// Other variables
--login-page-bg: url(data:image/gif;base64,R0lGODlhFAAhAKIAABwcHBsbGxcXFx0dHRUVFRkZGR4eHhQUFCH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4zLWMwMTEgNjYuMTQ2NzI5LCAyMDEyLzA1LzAzLTEzOjQwOjAzICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgRWxlbWVudHMgMTIuMCBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjhBOTg1NDlCNjMwNDExRUFBNEUwQ0M2RDdENUQ3RTBFIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjhBOTg1NDlDNjMwNDExRUFBNEUwQ0M2RDdENUQ3RTBFIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6OEE5ODU0OTk2MzA0MTFFQUE0RTBDQzZEN0Q1RDdFMEUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6OEE5ODU0OUE2MzA0MTFFQUE0RTBDQzZEN0Q1RDdFMEUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4B//79/Pv6+fj39vX08/Lx8O/u7ezr6uno5+bl5OPi4eDf3t3c29rZ2NfW1dTT0tHQz87NzMvKycjHxsXEw8LBwL++vby7urm4t7a1tLOysbCvrq2sq6qpqKempaSjoqGgn56dnJuamZiXlpWUk5KRkI+OjYyLiomIh4aFhIOCgYB/fn18e3p5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhYF9eXVxbWllYV1ZVVFNSUVBPTk1MS0pJSEdGRURDQkFAPz49PDs6OTg3NjU0MzIxMC8uLSwrKikoJyYlJCMiISAfHh0cGxoZGBcWFRQTEhEQDw4NDAsKCQgHBgUEAwIBAAAh+QQAAAAAACwAAAAAFAAhAAADfSi23M7igEcZOIfUSvApm1N42RAuQ0cqZ0Ri0xa8nlaltAdSY44RJsfAt3q4iLDGDFlj4Ji6RQ/6GwypqyOtwO12BQKv+EawnRq93dlwLa0NWtZpSYptniRzZfpSP9o0QBVaNHJKUHYoKkh6BnxIaoBMgnBYGAp0lgCLlgQJADs=);
Expand Down
47 changes: 27 additions & 20 deletions packages/common/src/shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,17 +132,18 @@ export type ConfigArgType = 'string' | 'int' | 'float' | 'boolean' | 'datetime'
export type DefaultFormComponentId =
| 'boolean-form-input'
| 'currency-form-input'
| 'customer-group-form-input'
| 'date-form-input'
| 'facet-value-form-input'
| 'json-editor-form-input'
| 'number-form-input'
| 'select-form-input'
| 'password-form-input'
| 'product-selector-form-input'
| 'customer-group-form-input'
| 'text-form-input'
| 'textarea-form-input'
| 'relation-form-input'
| 'rich-text-form-input'
| 'password-form-input'
| 'relation-form-input';
| 'select-form-input'
| 'text-form-input'
| 'textarea-form-input';

/**
* @description
Expand All @@ -151,40 +152,46 @@ export type DefaultFormComponentId =
* @docsCategory ConfigurableOperationDef
*/
type DefaultFormConfigHash = {
'boolean-form-input': {};
'currency-form-input': {};
'customer-group-form-input': {};
'date-form-input': { min?: string; max?: string; yearRange?: number };
'facet-value-form-input': {};
'json-editor-form-input': { height?: string };
'number-form-input': { min?: number; max?: number; step?: number; prefix?: string; suffix?: string };
'password-form-input': {};
'product-selector-form-input': {};
'relation-form-input': {};
'rich-text-form-input': {};
'select-form-input': {
options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
};
'boolean-form-input': {};
'currency-form-input': {};
'facet-value-form-input': {};
'product-selector-form-input': {};
'customer-group-form-input': {};
'text-form-input': {};
'textarea-form-input': {
spellcheck?: boolean;
};
'rich-text-form-input': {};
'password-form-input': {};
'relation-form-input': {};
};

export type DefaultFormComponentConfig<T extends DefaultFormComponentId> = DefaultFormConfigHash[T];

export type UiComponentConfig =
| ({ component: 'number-form-input' } & DefaultFormComponentConfig<'number-form-input'>)
| ({ component: 'date-form-input' } & DefaultFormComponentConfig<'date-form-input'>)
| ({ component: 'select-form-input' } & DefaultFormComponentConfig<'select-form-input'>)
| ({ component: 'text-form-input' } & DefaultFormComponentConfig<'text-form-input'>)
| ({ component: 'boolean-form-input' } & DefaultFormComponentConfig<'boolean-form-input'>)
| ({ component: 'currency-form-input' } & DefaultFormComponentConfig<'currency-form-input'>)
| ({ component: 'customer-group-form-input' } & DefaultFormComponentConfig<'customer-group-form-input'>)
| ({ component: 'date-form-input' } & DefaultFormComponentConfig<'date-form-input'>)
| ({ component: 'facet-value-form-input' } & DefaultFormComponentConfig<'facet-value-form-input'>)
| ({ component: 'json-editor-form-input' } & DefaultFormComponentConfig<'json-editor-form-input'>)
| ({ component: 'number-form-input' } & DefaultFormComponentConfig<'number-form-input'>)
| ({ component: 'password-form-input' } & DefaultFormComponentConfig<'password-form-input'>)
| ({
component: 'product-selector-form-input';
} & DefaultFormComponentConfig<'product-selector-form-input'>)
| ({ component: 'customer-group-form-input' } & DefaultFormComponentConfig<'customer-group-form-input'>)
| { component: string; [prop: string]: Json };
| ({ component: 'relation-form-input' } & DefaultFormComponentConfig<'relation-form-input'>)
| ({ component: 'rich-text-form-input' } & DefaultFormComponentConfig<'rich-text-form-input'>)
| ({ component: 'select-form-input' } & DefaultFormComponentConfig<'select-form-input'>)
| ({ component: 'text-form-input' } & DefaultFormComponentConfig<'text-form-input'>)
| ({ component: 'textarea-form-input' } & DefaultFormComponentConfig<'textarea-form-input'>)
| { component: string; [prop: string]: any };

export type CustomFieldsObject = { [key: string]: any };

Expand Down
19 changes: 17 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6584,6 +6584,11 @@ code-point-at@^1.0.0:
resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=

codejar@^3.5.0:
version "3.5.0"
resolved "https://registry.npmjs.org/codejar/-/codejar-3.5.0.tgz#be3a6a77b4c422998e56710ca854d166f8507eb2"
integrity sha512-uXrFZZ+yb23YY7+WtTux2Yyokt+Lty/kBnW/OhhEGp8IW8/lrJw5Gs1wwCyt2vpMfsVdudLmV5xAgYqsZY/49A==

codelyzer@^6.0.0:
version "6.0.2"
resolved "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.2.tgz#25d72eae641e8ff13ffd7d99b27c9c7ad5d7e135"
Expand Down Expand Up @@ -10120,7 +10125,7 @@ ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==

ignore-walk@^3.0.1:
ignore-walk@^3.0.1, ignore-walk@^3.0.3:
version "3.0.4"
resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335"
integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==
Expand Down Expand Up @@ -13844,14 +13849,24 @@ [email protected], npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-packa
semver "^7.3.4"
validate-npm-package-name "^3.0.0"

npm-packlist@1.1.12, npm-packlist@^1.1.6, npm-packlist@^2.1.4:
npm-packlist@^1.1.6:
version "1.1.12"
resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a"
integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==
dependencies:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"

npm-packlist@^2.1.4:
version "2.2.2"
resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz#076b97293fa620f632833186a7a8f65aaa6148c8"
integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg==
dependencies:
glob "^7.1.6"
ignore-walk "^3.0.3"
npm-bundled "^1.1.1"
npm-normalize-package-bin "^1.0.1"

[email protected], npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1:
version "6.1.1"
resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148"
Expand Down

0 comments on commit 4297b87

Please sign in to comment.