Skip to content

Commit

Permalink
Merge branch 'main' into feature/MEDITOR-858-bulk-UI
Browse files Browse the repository at this point in the history
  • Loading branch information
KrupaRami authored Oct 21, 2024
2 parents ebbd1e2 + f36002a commit dc28b0c
Show file tree
Hide file tree
Showing 27 changed files with 521 additions and 64 deletions.
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.10.1] - 2024-09-30

### Fixed

- `DateTimeWidget` now registers input through keyboard (typing, copy/paste).

## [1.10.0] - 2024-06-04

### Added

- `/api/models/[modelName]/validate` endpoint for strict validation of documents against their model's schema

## [1.9.13] - 2024 05-29

### Added

- changelog

### Fixed

- add NATS listeners only when there's not an existing NATS client
3 changes: 1 addition & 2 deletions docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.3"

services:
notebookviewer:
container_name: meditor_notebookviewer
Expand Down Expand Up @@ -37,6 +35,7 @@ services:
volumes:
- ./packages/app:/usr/src/app
- /usr/src/app/node_modules
- /usr/src/app/.next

docs:
container_name: meditor_docs
Expand Down
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.3"

services:
notebookviewer:
depends_on:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "meditor",
"version": "1.9.12",
"version": "1.10.1",
"description": "mEditor, the model editor",
"directories": {
"example": "examples",
Expand Down
3 changes: 2 additions & 1 deletion packages/app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

EXPOSE 3000
EXPOSE 4000

CMD if [ "$NODE_ENV" = "production" ]; \
then \
npm run build && npm run start; \
npm run start; \
else \
npm run dev; \
fi
44 changes: 44 additions & 0 deletions packages/app/components/jsonschemaform/widgets/AnchorWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { WidgetProps } from '@rjsf/utils'
import { useEffect, useState } from 'react'
import { findAndReplace, macros } from './utils'

/**
* Schema Layout:
"MyProperty": {
"ui:widget": "anchor",
"ui:options": {
"href": "http://localhost:8080/service-request-from-config?variable_entry_id=******",
"text": "",
"change": {
"every": "***",
"to": ["FIELD_LOOKUP:EntryID", "FIELD_LOOKUP:VariableEntryID"] // replace with no regex, not replaceAll, is used. This means you can stack "every" to replace per "to" field.
}
}
},
*/
export default function AnchorWidget({ options }: WidgetProps) {
const [href, setHref] = useState(options.href as string)

useEffect(() => {
// @ts-expect-error
if (!!options.change.every) {
setHref(
findAndReplace(
options.href as string,
// @ts-expect-error
options.change.every as string,
// @ts-expect-error
options.change.to as string[],
macros
)
)
}
// @ts-expect-error
}, [options.href, options.change.every, options.change.to, setHref])

return (
<a href={href} target="_blank" rel="noopener noreferrer">
{options.text || href}
</a>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function CKEditorWidget(props: WidgetProps) {
event.editor.setReadOnly(props.readonly || false)
}}
onBeforeLoad={CKEDITOR => {
CKEDITOR.config.versionCheck = false
CKEDITOR.disableAutoInline = true

registerPluginsWithCkEditorInstance(CKEDITOR)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react'
import type { WidgetProps } from '@rjsf/utils'
import { getTemplate } from '@rjsf/utils'
import { ID_PREFIX } from './constants'

const DEFAULT_DELIMETER = ' > '
const ID_PREFIX = 'root_'

export default function ConcatenatedWidget(props: WidgetProps) {
const BaseInput = getTemplate('BaseInputTemplate', props.registry)
Expand Down
43 changes: 25 additions & 18 deletions packages/app/components/jsonschemaform/widgets/DateTimeWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ import Flatpickr from 'react-flatpickr'
import { format, zonedTimeToUtc } from 'date-fns-tz'
import type { WidgetProps } from '@rjsf/utils'

function formatDate(date: string, dateFormatOption: any) {
let dateValue

switch (dateFormatOption) {
case 'Z':
dateValue = zonedTimeToUtc(new Date(date), 'Etc/UTC').toISOString()
break

default:
dateValue = format(new Date(date), 'yyyy-MM-dd HH:mm:ssxxx')

break
}

return dateValue
}

function DateTimeWidget(props: WidgetProps) {
const {
id,
Expand All @@ -28,25 +45,15 @@ function DateTimeWidget(props: WidgetProps) {
disabled={disabled || readonly}
autoFocus={autofocus || false}
defaultValue={value}
onBlur={() => {
onBlur(id, value)
}}
onChange={(_selectedDates, date) => {
let dateValue

switch (dateFormatOption) {
case 'Z':
dateValue = zonedTimeToUtc(
new Date(date),
'Etc/UTC'
).toISOString()
break

default:
dateValue = format(new Date(date), 'yyyy-MM-dd HH:mm:ssxxx')
onBlur={(event: any) => {
// @rsjf (or maybe our onBlur function) does not correctly handle the blur event for this component.
// Instead of onBlur, call onChange if there's a value.
if (event.target.value) {
onChange(formatDate(event.target.value, dateFormatOption))
}

onChange(dateValue)
}}
onChange={(_selectedDates: any, date: string) => {
onChange(formatDate(date, dateFormatOption))
}}
options={{
time_24hr: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { findAndReplace } from '../utils'

/**
"href": "http://localhost:8080/service-request-from-config?variable_entry_id=******",
"text": "",
"change": {
"every": "***",
"to": ["FIELD_LOOKUP:EntryID", "FIELD_LOOKUP:VariableEntryID"] // replace with no regex, not replaceAll, is used. This means you can stack "every" to replace per "to" field.
}
*/
describe(`RJSF Widgets`, () => {
describe(`findAndReplace`, () => {
const sourceString =
'http://localhost:8080/service-request-from-config?variable_entry_id=***'
const every = '***'
const to = ['a-variable-entry-id']
const macros = {
FIELD_LOOKUP: () => 'this value was returned',
} as const

test(`replaces a single instance with a single value, no macro`, () => {
const value = findAndReplace(sourceString, every, to, macros)

expect(value).toMatchInlineSnapshot(
`"http://localhost:8080/service-request-from-config?variable_entry_id=a-variable-entry-id"`
)
})

test(`can repeat multiple replacement for multiple matches`, () => {
const sourceString =
'http://localhost:***/service-request-from-config?variable_entry_id=******'
const to = ['8080', 'foo', 'bar']
const value = findAndReplace(sourceString, every, to, macros)

expect(value).toMatchInlineSnapshot(
`"http://localhost:8080/service-request-from-config?variable_entry_id=foobar"`
)
})

test(`can use macros`, () => {
// Our FIELD_LOOKUP macro does nothing but return a string, unlike the real macro.
const to = ['FIELD_LOOKUP']
const value = findAndReplace(sourceString, every, to, macros)

expect(value).toMatchInlineSnapshot(
`"http://localhost:8080/service-request-from-config?variable_entry_id=this value was returned"`
)
})

test(`will throw if macro does not have a handler`, () => {
const to = ['NOT_A_HANDLED_MACRO']
const macros = {
NOT_A_HANDLED_MACRO: () => `this doesn't have a handler`,
} as const

expect(() => {
findAndReplace(sourceString, every, to, macros)
}).toThrowErrorMatchingInlineSnapshot(
`"Handler does not yet have a case to handle this macro: NOT_A_HANDLED_MACRO"`
)
})

test(`will match per "to" entry found, but no further`, () => {
const value = findAndReplace(sourceString, every, to, macros)

expect(value).toMatchInlineSnapshot(
`"http://localhost:8080/service-request-from-config?variable_entry_id=a-variable-entry-id"`
)
})

test(`will not replace on empty "to" arrays`, () => {
const to = []
const value = findAndReplace(sourceString, every, to, macros)

expect(value).toMatchInlineSnapshot(
`"http://localhost:8080/service-request-from-config?variable_entry_id=***"`
)
})

test(`will not replace on empty "every" string`, () => {
const every = ''
const value = findAndReplace(sourceString, every, to, macros)

expect(value).toMatchInlineSnapshot(
`"a-variable-entry-idhttp://localhost:8080/service-request-from-config?variable_entry_id=***"`
)
})

test(`returns unmodified source string when no matches are found`, () => {
const sourceString =
'http://localhost:8080/service-request-from-config?variable_entry_id=^^^'
const value = findAndReplace(sourceString, every, to, macros)

expect(value).toEqual(sourceString)
})
})
})
3 changes: 3 additions & 0 deletions packages/app/components/jsonschemaform/widgets/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const ID_PREFIX = 'root_'

export { ID_PREFIX }
2 changes: 2 additions & 0 deletions packages/app/components/jsonschemaform/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import DateTimeWidget from './DateTimeWidget'
import ConcatenatedWidget from './ConcatenatedWidget'
import HtmlTextWidget from './HtmlTextWidget'
import TitlePropertyWidget from './TitlePropertyWidget'
import AnchorWidget from './AnchorWidget'

const widgets: RegistryWidgetsType = {
ckeditor: CKEditorWidget,
Expand All @@ -17,6 +18,7 @@ const widgets: RegistryWidgetsType = {
concatenated: ConcatenatedWidget,
htmltext: HtmlTextWidget,
titleproperty: TitlePropertyWidget,
anchor: AnchorWidget,
}

export default widgets
47 changes: 47 additions & 0 deletions packages/app/components/jsonschemaform/widgets/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ID_PREFIX } from './constants'

const macros = {
FIELD_LOOKUP: (name: string) => {
let el = document.getElementById(`${ID_PREFIX}${name}`) as HTMLInputElement
// This may be run before the element has been rendered to the DOM, so don't return `undefined`.
const value = el.value ?? ''

return globalThis.encodeURIComponent(value)
},
} as const

function findAndReplace(
sourceString: string = '',
every: string = '',
to: string[] = [],
macros: Record<string, Function> = {}
) {
// Starting from the source string, iterate through every entry in "to", replacing with either a macro's return value or a value.
return to.reduce((accumulator, current) => {
// We may have a MACRO_NAME:FieldName combo, or we may have a value.
const [macroNameOrValue, maybeFieldName] = current.split(':')
const maybeMacro = macros[macroNameOrValue]

if (maybeMacro) {
// We know we have a macro now.
const macroName = macroNameOrValue
const fieldName = maybeFieldName

switch (macroName) {
case 'FIELD_LOOKUP':
const replacementValue = macros[macroName](fieldName)

return accumulator.replace(every, replacementValue)

default:
throw Error(
`Handler does not yet have a case to handle this macro: ${macroName}`
)
}
} else {
return accumulator.replace(every, current)
}
}, sourceString)
}

export { findAndReplace, macros }
14 changes: 13 additions & 1 deletion packages/app/components/search/search-result.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@
justify-content: center;
}

.result > div > input {
.result > div > div > input {
margin-right: 10px;
}

.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Loading

0 comments on commit dc28b0c

Please sign in to comment.