Separate forms app#1608
Conversation
work in progress - do not merge
🦋 Changeset detectedLatest commit: 10d4efc The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
latin-panda
left a comment
There was a problem hiding this comment.
It's great to see this coming along! I did an initial review, and tomorrow I'll take another deep look.
| configureServer(server) { | ||
| server.middlewares.use((req, res, next) => { | ||
| // must match the regex paths defined in Nginx | ||
| const newSubmissionUrlRegex = /^\/projects\/\d+\/forms\/[^/]+(?:\/draft)?(?:\/preview|\/submissions\/new(?:\/offline)?\/?|\/submissions\/[^/]+\/edit)\/?/; |
There was a problem hiding this comment.
Same feedback from here
Is there a way to define this in one place and share it in the different configs?
There was a problem hiding this comment.
I don't know if that'll be possible between nginx and vue, but I'm sure we can at least get the nginx using the same config. There's an ongoing convo about this because the mismatch between dev and prod is risky. I think the docker work will help us move in that direction.
I've added comments and renamed the regexes to be more clear.
| instanceId: String | ||
| }); | ||
|
|
||
| const emit = defineEmits(['loaded']); |
There was a problem hiding this comment.
Who calls this emit? Is it dead code?
| const { status, statusText, headers } = response; | ||
|
|
||
| const fetchHeaders = new Headers(); | ||
| for (const [key, value] of Object.entries(headers)) { |
There was a problem hiding this comment.
The response comes from the native fetch, so the headers are a Headers type that should be iterated using its entries function.
| for (const [key, value] of Object.entries(headers)) { | |
| for (const [key, value] of headers.entries()) { |
| router.beforeEach(to => { | ||
| if (to.fullPath.startsWith('/#/')) { | ||
| // This must do a full page refresh because the page may end up in a different vue app | ||
| window.location.href = to.fullPath.substring(2); |
There was a problem hiding this comment.
The user's back button takes them to the hash URL, which redirects again. The replace swaps the entry.
Also, it can return false here to cancel any forward loading in central (if any callback is reacting to navigation events)
| window.location.href = to.fullPath.substring(2); | |
| window.location.replace(to.fullPath.slice(2)); | |
| return false; |
There was a problem hiding this comment.
It turns out returning false cancels the request so which caused playwright to bail. Because this isn't a security issue I've left it continuing on which might be a little bit of a waste but means the request completes cleanly.
|
|
||
| #web-form-renderer-submission-modal pre { | ||
| white-space: pre-wrap; | ||
| if (isEdit.value) { |
There was a problem hiding this comment.
Preferably wrap in an init function or onMount hook
| @@ -0,0 +1,32 @@ | |||
| <!DOCTYPE html> | |||
| <!-- | |||
| Copyright 2017 ODK Central Developers | |||
There was a problem hiding this comment.
There have been conversations in Slack about this (maybe in the team channel?). I think we resolved to drop this for new development, since this is a new app. It could be dropped.
There was a problem hiding this comment.
Did you decide to leave it here and remove it from the other files? Or perhaps it has not been pushed yet? 😅
There was a problem hiding this comment.
Ok! Removed that one too.
|
@latin-panda I've resolved all your comments. I've also...
|
latin-panda
left a comment
There was a problem hiding this comment.
I haven't finished yet, but I'm submitting the first part of today's review
| let body; | ||
| const contentType = headers.get('content-type'); | ||
| if (contentType && (contentType.includes('application/json') || contentType.includes('application/geo+json'))) { | ||
| body = await response.json(); |
There was a problem hiding this comment.
In L254, this body is passed to the Response constructor, but it doesn't accept the data type that .json() produces and ends up converting it to a string ("[object Object]"). That's probably why it initially used JSON.stringify(body).
There was a problem hiding this comment.
Oh wow... looking at this now I don't think we need this function at all. It was used to transform an axios response into a native response, but because it's all native that's not necessary.
Removed!
| url = `/v1/projects/${props.form.projectId}/forms/${props.form.xmlFormId}/submissions/${props.instanceId}`; | ||
| method = 'PUT'; | ||
| } else { | ||
| url = `/v1/projects/${props.form.projectId}/forms/${props.form.xmlFormId}${draftPath}/submissions`; |
There was a problem hiding this comment.
When does it append a st query parameter for public-link?
There was a problem hiding this comment.
Same question for L59, L151, L262, 268, 279.
What do you think about having a URL builder and unit testing it? Looking to centralize it
There was a problem hiding this comment.
As far as I can tell it's only used for fetching the formXml which is handled in the api. After that the st isn't needed.
Regarding centralising it, I think that's a good idea. At the moment these are only used once each so I just inlined them.
| clearFormCallback({ next: POST_SUBMIT__NEW_INSTANCE }); | ||
| }; | ||
| const submissionPath = () => { | ||
| return `/v1/projects/${props.form.projectId}/forms/${props.form.xmlFormId}/submissions/${props.instanceId}`; |
There was a problem hiding this comment.
The suffix is used only for api calls, for L116 we need it without /v1/
There was a problem hiding this comment.
Good catch! Amazing that it worked at all... removed now.
| if (response.ok) { | ||
| const data = await response.json(); | ||
| result = { success: true, data }; | ||
| } |
There was a problem hiding this comment.
It should assign result when response.ok is false to ensure consistent handling in subsequent flows after this code.
| submissionModal.hide(); | ||
| const getAttachment = (requestUrl: URL) => { | ||
| const encodedName = encodeURIComponent(requestUrl.pathname.split('/').pop()!); | ||
| const draftPath = ''; |
There was a problem hiding this comment.
In L64, there's a check for props.form.draft that we could use here as well.
Or does it even need draftPath? What happens if there are attachments for form drafts?
There was a problem hiding this comment.
You're right! It is expected by the API. Fixed.
| } catch (e) { | ||
| if (e instanceof RequestError && e.statusCode >= 401 && e.statusCode < 404) { | ||
| // not logged in | ||
| window.location.href = '/login?next=/wf' + window.location.pathname; |
There was a problem hiding this comment.
I think it needs to preserve window.location.search for the navigation after login
| xform.value = await getFormXml(formConfig.projectId, formConfig.xmlFormId, formConfig.draft, st) | ||
| webFormsEnabled.value = true; | ||
| } else { | ||
| if (offline) { |
There was a problem hiding this comment.
Little comment to not lose track.
| if (offline) { | |
| if (offline) { | |
| // TODO: Update once Web Forms has support for offline |
| <template v-else-if="webFormsEnabled"> | ||
| <WebFormRenderer :form="form!" :xform="xform!" :instance-id="instanceId" :action-type="props.actionType ?? 'new'"/> | ||
| </template> | ||
| <template v-else> | ||
| <EnketoIframe :form="form!" :enketo-id="enketoId" :action-type="props.actionType ?? 'new'"/> | ||
| </template> |
There was a problem hiding this comment.
It doesn't seem right to default to new here. Instead of silently prompting the user to create a new submission, it would be better to display an error message and an option to close the page. For example, if editing isn't supported by Enketo, it could show: Editing is only supported for Web Forms. Please contact the administrator.
There was a problem hiding this comment.
I think defaulting to 'new' is correct in the case when no action type is provided.
However you're right that we should catch the case where they're editing in Enketo. I've caught that out and given a non-specific error above (404). A specific error is new functionality - I'm happy to raise an issue if you think it's worth that.
| "vue-i18n": "^10.0.7", | ||
| "vue-router": "^4.6.3" | ||
| }, | ||
| "devDependencies": { |
There was a problem hiding this comment.
Is there a way we can declare primevue and the local packages (web-forms, xforms-engine) as direct dependencies here? It helps a lot with readability so we know exactly what the app consumes.
There was a problem hiding this comment.
Maybe not... 🤔
Leaving the comment open just in case there's an idea
There was a problem hiding this comment.
I've added primevue because that one we should definitely have.
I'm unsure whether we should include monorepo packages in the dependencies...
| const url = `/v1/projects/${projectId}/forms/${formId}${draftPath}.xml${qs}`; | ||
| const response = await fetch(url); | ||
| if (!response.ok) { | ||
| const result = await response.json(); |
There was a problem hiding this comment.
If response.json() breaks (response.ok is falsy, maybe corrupted), is there any other catch to handle it nicely?
There was a problem hiding this comment.
I don't think so. We could catch it here and wrap it up with a 500 error or something? But I think letting it go and be handled by the caller is ok too.
There was a problem hiding this comment.
| if (returnUrlPascalCase && typeof returnUrlPascalCase === 'string') { | |
| return returnUrlPascalCase; | |
| } | |
| if (returnUrl && typeof returnUrl === 'string') { | |
| return returnUrl; | |
| } | |
| return null; |
| <script setup lang="ts"> | ||
| import { ref, defineAsyncComponent } from 'vue'; | ||
| import { useRoute, useRouter } from 'vue-router'; | ||
| import { type Form, getFormByEnketoId, getFormByFormId, getFormXml, getProject, type Project, queryString, RequestError } from '../utils/api.ts'; |
There was a problem hiding this comment.
Is lint or prettify running? This is very long. It should have been caught and fixed automatically
There was a problem hiding this comment.
At the moment it's using central's config for eslint, and no prettify at all. As most of the code came from central I took the path of least resistance. I've manually broken the line here.
I'm looking forward to getting web-forms, forms, and central on the same setting.
| }); | ||
| if (inst) { | ||
| inst.appContext.app.use(PrimeVue, { theme: { preset: odkThemePreset, options: { darkModeSelector: false } } }); | ||
| // TODO this should work instead of the above, but haven't got it working yet... |
There was a problem hiding this comment.
Is this planned for this round or for a follow-up PR?
There was a problem hiding this comment.
At the moment it's working so I'm ok leaving it in. If you have any ideas please let me know!
| const handleSubmit = async ( | ||
| payload: MonolithicInstancePayload, | ||
| clearFormCallback: Function | ||
| ) => { |
There was a problem hiding this comment.
The spacing looks off to me
| const handleSubmit = async ( | |
| payload: MonolithicInstancePayload, | |
| clearFormCallback: Function | |
| ) => { | |
| const handleSubmit = async ( | |
| payload: MonolithicInstancePayload, | |
| clearFormCallback: Function | |
| ) => { |
| if (isEdit.value) { | ||
| fetchSubmissionAttachments().then(() => { | ||
| loading.value = false; | ||
| }) |
There was a problem hiding this comment.
I think lint catches these too
| }) | |
| }); |
There was a problem hiding this comment.
Linting is a mess... I'm using the old central eslint config but now because I'm using typescript it's not happy. I'm going to look at how hard it would be to conform with web-forms style...
There was a problem hiding this comment.
Ok I've switched to using the same config as web-forms packages...
| export const i18n = createI18n({ | ||
| locale: fallbackLocale, | ||
| fallbackLocale, | ||
| messages: { }, |
There was a problem hiding this comment.
Is it correct that messages is empty?
There was a problem hiding this comment.
All the messages required are inline in the vue files. This will change if/when we modernise the i18n.
| import { i18n } from './i18n' | ||
|
|
||
| const app = createApp(Forms as Component); | ||
| app.use(webFormsPlugin); |
There was a problem hiding this comment.
Why defining Primevue in web-form-renderer.vue L35 instead of here main.ts? 🤔
There was a problem hiding this comment.
Because primevue isn't needed for enketo rendering so I wanted to leave it out if possible.
| @@ -0,0 +1,32 @@ | |||
| <!DOCTYPE html> | |||
| <!-- | |||
| Copyright 2017 ODK Central Developers | |||
There was a problem hiding this comment.
Did you decide to leave it here and remove it from the other files? Or perhaps it has not been pushed yet? 😅
| "vue-i18n": "^10.0.7", | ||
| "vue-router": "^4.6.3" | ||
| }, | ||
| "devDependencies": { |
There was a problem hiding this comment.
Maybe not... 🤔
Leaving the comment open just in case there's an idea
| case 'forms.js': return size > 2_000_000; | ||
| case 'web-form.js': return size > 2_000_000; // Matches web-form-renderer.js |
There was a problem hiding this comment.
Is this L63 the web forms plugin or the web-forms renderer of app/forms? If the latter, it can be lowered. If the former, we need to update the comment.
When running the script:
//:build:web: dist/assets/web-form-renderer-CycNBHBr.js 985.19 kB │ gzip: 291.12 kB
//:build:web: dist/forms/forms-fTznoukg.js 1,848.76 kB │ gzip: 779.64 kB
There was a problem hiding this comment.
Good catch. I reduced the size to 1MB.
There was a problem hiding this comment.
Funny side note: The reason it matches web-form-renderer.js is because the word "renderer" is 8 letters long which exactly matches the regex which is designed to strip off the hash suffix.
|
@latin-panda Thank you for your continued reviews! I've responded to your comments, and this is good to go again. |
Things left to do...
getodk/web-forms#535
What has been done to verify that this works as intended?
Why is this the best possible solution? Were any other approaches considered?
How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
Does this change require updates to user documentation? If so, please file an issue here and include the link below.
Before submitting this PR, please make sure you have:
npm run testandnpm run lintand confirmed all checks still pass OR confirm CircleCI build passesnpm run changesetto generate a changeset file for changes that should be included in the release notes