Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboard fixes and refactors #3614

Merged
merged 12 commits into from
Oct 7, 2024
Merged

Dashboard fixes and refactors #3614

merged 12 commits into from
Oct 7, 2024

Conversation

jcushman
Copy link
Contributor

@jcushman jcushman commented Oct 6, 2024

Changes made while responding to Becky's QA with developer-playground = true:

With the waffle flag developer_playground = false, submit an invalid URL, so that you are offered the chance to upload your own file. Click the button. Nothing happens: I think this feature has not yet been fully implemented in Vue. Set the flag true. Repeat. A modal DOES appear. But, it is clearly buggy/unfinished. (Also, like the Vue link batch modal, it is weirdly skinny.)

New upload modal implemented.

Select an empty folder. Observe that the link list overflows and scrolls horizontally.
Log out, then log in again as an admin, org, or registrar user. No folder will be selected. Observe that the UI reports, "This is an empty folder". Repeat in prod or with the flag off, observe that it doesn't.

Fixed up status messages for folders:

  • Simpler HTML if showing status message, to avoid scrollbars.
  • "No folder selected." if no folder selected.
  • Spinner while loading, instead of unstyled "Loading" text.

In prod, select a folder with links. Click into the link list's search box. Press tab until keyboard focus is on the caret button that will expand the first link. Press enter to expand the link. Observe that keyboard focus remains on the button. Press it again to close, and observe that focus remains again. Repeat the process with this branch; observe that both times, keyboard focus is dropped. (Your browser might help you out and set it somewhere close by; browser behavior here varies.)

If using keyboard, focus stays on caret button when opening/closing. If using mouse to click the rest of the row, focus goes to first input box on open, and does nothing on close.

The label for the caret button that toggles a link's details include "{{ link.guid }}", verbatim, instead of that variable's value.

Fixed.

Other changes in this PR:

  • Refactored spinner component that is easier to use in layouts.
  • Refactored existing form components made for WIP upload form.
  • Cleaned up capture button styles -- removed focus outline while capturing, fixed up padding, progress bar replaces spinner.
  • Some globalStore cleanup -- moved some values from store to props, used a more consistent style for storing references to components.
  • Improved the fetchDataOrError fetch wrapper, and used consistently in place of fetch() and one-off error handling.
  • Fixed jstree drag and drop style -- turns out this was messed up because of having body { height: 100% } for a sticky footer, meaning our window never really scrolled. Added a drag and drop indicator with a little more style.

I did my best to break these all out into separate commits, but probably mis-organized a line or two.

- takes up space in the DOM, easier to style
- horizontally centered
- uses single config object for spin.js params
- default aria attributes
- fetchDataOrError accepts data option that can be either formData or json data
- fetchDataOrError returns response data, if any, on error, and provides access to response object
body { height: 100% } causes event.pageX and event.pageY not to work, because body scrolls rather than window scrolling
- show spinner instead of unstyled text while loading
- show "No folder selected" instead of empty div when no folder selected
- captureGUID and captureErrorMessage as props instead of global state
- CreateLink uses fetch wrapper
@jcushman jcushman requested a review from a team as a code owner October 6, 2024 20:14
@jcushman jcushman requested review from teovin and removed request for a team October 6, 2024 20:14
Copy link

codecov bot commented Oct 6, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 69.25%. Comparing base (c72853c) to head (2abbcda).
Report is 6 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3614      +/-   ##
===========================================
+ Coverage    69.19%   69.25%   +0.05%     
===========================================
  Files           53       54       +1     
  Lines         7193     7207      +14     
===========================================
+ Hits          4977     4991      +14     
  Misses        2216     2216              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@teovin teovin requested review from rebeccacremona and removed request for teovin October 7, 2024 14:40
Copy link
Contributor

@rebeccacremona rebeccacremona left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whew! This is a lot. I gave it all a read-through, but did not pull down to test. I had one question about something possibly missed during refactoring, and then just made a couple remarks. Since this is behind the feature flag, let's QA in stage 🙂

Thanks for taking on this project!

for (const [key, { value }] of Object.entries(formData.value)) {
formDataObj.append(key, value);
formDataObj.append('file', file.value);
if (!props.captureGUID) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noting that the non-Vue version of this form actually doesn't do the patch version of this: it always makes a new Perma Link.

So, we'll have to see if no title/description/url, ends up being what users want here. Could end up that they prefer the current behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah huh, I didn't actually trigger that version in my testing, just did captures of invalid domains. How does one show it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the quickest way is, you could throw a 1/0 or raise HaltCaptureException someplace in https://github.com/harvard-lil/perma/blob/develop/perma_web/perma/celery_tasks.py#L312, to make a failed capture.

if (error) {
console.log(error, response)
if (data) {
errors.value = data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this want to be globalErrors.value, like below, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically no -- the idea here is the API would return 400 with data {title: "Terrible title"} and it would render the same way as the validation section previously in the function. Again I'm not sure what states would trigger that to test, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

// This appears to give us a sticky footer without breaking anything.
// But I'm not sure about it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL I love this comment.

onMounted(() => {
globalStore.components.createLink = getCurrentInstance().exposed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen this pattern before. Is it a way to let components communicate with each other?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I made this one up, but the thought process was ...

  • Vue components have this nice way to encapsulate inputs and outs -- defineProps shows you what settings the parent component can set and two-way data bindings, and defineExpose shows you what methods the parent can poke. (globalState pokes holes in that whole encapsulation, so we always use that by way of a necessary or convenient evil.)
  • So for the direct parent, communication is easy -- you embed <Child ref=child props...>, and use child.method() to poke child.
  • Oh but then it turns out some extended relative needs to poke child sometimes too. So parent will store it as globalState.childRef and the relative can do globalState.childRef.method(). Alternatively we can have relative update a globalState variable and child watch it, which gets encapsulated in "event bus" libraries -- but if we're only going to have one instance of Child, that feels like unnecessarily poking holes in Child's defineProps/defineExpose interface.
  • Then after repeating this a few times, we have globalState.childRef and globalState.kidInstance and globalState.widget and it starts to feel messy, so what if we tidy them all up under a globalState.components dictionary and have the children register themselves instead of making their parents do it?

I think all of this makes sense and ends up flowing pretty naturally, except that it's not really proper for the children to register themselves, because the parents are the ones in a position to know how many instances there are. So it'd probably be better to do const childRef = ref(); globalState.components.child = childRef in the parent. I can switch to that if you think it's worth updating.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This would also have the virtue that globalState.components.child = childRef is easier to read than globalStore.components.me = getCurrentInstance().exposed)

Copy link
Contributor

@rebeccacremona rebeccacremona Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! Thanks for taking the time to walk me through it. It does seem like a tidy alternative to "we can have relative update a globalState variable and child watch it", if maybe a bit idiosyncratic? But no objections here! No opinion on switching things so the parent registers the child/children or not; I think I understand your argument that having the parent do it is more correct; I could believe this way is easier read, if it's not spread across files, maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

globalState.components.child = childRef does look MUCH less fancy than globalStore.components.me = getCurrentInstance().exposed 🙂

@jcushman jcushman merged commit 945e8d5 into harvard-lil:develop Oct 7, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants