-
Notifications
You must be signed in to change notification settings - Fork 92
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
Resolve name conflicts in packages #3596
base: master
Are you sure you want to change the base?
Conversation
…nto resolve-name-conflicts
Codecov Report
@@ Coverage Diff @@
## master #3596 +/- ##
==========================================
- Coverage 36.14% 36.05% -0.09%
==========================================
Files 676 677 +1
Lines 29673 29754 +81
Branches 4321 4340 +19
==========================================
+ Hits 10724 10727 +3
- Misses 17812 17883 +71
- Partials 1137 1144 +7
Flags with carried forward coverage won't be shown. Click here to find out more.
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
I deployed it to searchminimal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, I'd like to point out that this feature is essential and we were missing it for so long. However, I feel like the current implementation is not yet ready for the prime time.
-
Here's some UX issues / suggestions that are (probably) easy to address:
-
In the file rename dialog enter triggers package push -- probably should trigger only rename instead.
-
In the file rename dialog "submit" button label seems too generic, "rename" or smth would be more appropriate.
-
Instead of always succeeding, I think it's better to check if the name is already taken and disallow submitting to prevent automatic rename (which is kinda confusing when it happens).
-
Instead of treating conflicts as errors, maybe it would make more sense to just show warnings when auto-renaming files, and don't block submit.
-
When there are conflicts, "files should match schema" error message is shown, which is confusing and doesn't reflect reality.
-
Auto-renaming is sometimes buggy and leads to smth like
file (conflict 1) (conflict 1).ext
instead offile (conflict 2).ext
. -
(Nice-to-have, tangential) Consider adding a tooltip describing the state of an entry (e.g. added from s3, removed, renamed from $key, etc. showing the physical key may be useful as well).
-
(Nice-to-have, tangential) Adding an option to toggle deleted files visibility may be useful to reduce visual clutter, tho it feels orthogonal and somewhat out of scope.
-
-
My main complaint, tho, is I feel like we're stretching the model too far and the abstractions start leaking -- this makes the logic harder to follow and less predictable, ultimately leading to unexpected errors and difficulties maintaining this code and developing it further. So my advice is to adjust the model to accommodate the new requirements and make it more consistent. Some suggestions:
-
Use composition: contain / reference File objects instead of mutating (extending) them directly.
-
"renamed" state implementation is inconsistent with the model, since the model only has the notion of existing / added / deleted entries. Instead of tracking this as "changed" prop, maybe on first rename we should (1) "add" an entry with the new logical key and set "renamedFrom" (or smth like that) prop on it pointing to the original logical key; and (2) mark the original entry as deleted -- this feels more in-line with our current state model and UX. The added entry with "renamedFrom" prop may be rendered with the "revert" action instead of "remove".
-
const color = React.useMemo(() => { | ||
if (!disabled && state !== 'invalid' && !R.isNil(value) && !R.isEmpty(value)) { | ||
return 'primary' | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's the reason for implicitly returning undefined
instead of explicit 'inherit'
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just saved a couple of symbols, and done this consistently with EditFileName.tsx
I think it just unacceptable to submit files with auto-renaming files. It's unexpected for user, that he added a file with one name, but it was submitted with another name. Warning is easy to miss.
Yes, good idea and probably easy. First, I interpreted it differently, and it could take a week :)
Ok, but I couldn't reproduce, I'll try to find it in code
Good idea, I'll implement it in standalone PR
Hmm, could be useful, but I never felt I need this |
On model Yes, I agree, I tried to fit into model something that it wasn't designed for. I have two excuses and two solutions. I wanted to do a minimal change, to have better chances to fit into horizon myself and to fit review into horizon. And I wanted to do a feature separately from refactoring. It's a large separate topic why it's a good idea to make it work first, then make it good (but of course, only if we don't forget and invest time in making good). I suggest merging this (after addressing UX issues and bugs) and work on refactoring in a separate PR. Also, there is a spec on refactoring file types https://github.com/quiltdata/specs/pull/53, it, for example, has the same proposal to contain file in a separate property. I'll split the spec into three specs as noted in description |
|
Co-authored-by: Alexei Mochalov <[email protected]>
…nto resolve-name-conflicts
well, in the end it did not fit, so i don't think we should rush and try to fit it anyways in some half-baked form
it's ok (in my book) to make it work and then make it good before shipping. i can't agree that breaking things, shipping them, and then fixing them is a valid approach. i'm kinda ok with expanding the model and postponing a proper refactoring, but i'm not ok with breaking the model, so i still feel like this requires addressing:
since the current approach is too brittle and has edge cases that aren't handled |
i realize that i am accountable for this as well to some extent (as a reviewer). @drernie @akarve you can override and approve this PR if you think it's more important to get this out the door sooner and backlog the polish / refactoring, but i'm convinced that we should not compromise on quality. scope hammering is not compromising on quality, it's compromising on, well, scope, and this feature didn't make it. |
@@ -25,10 +29,11 @@ function Dialog({ | |||
const error = React.useMemo(() => validate(value), [validate, value]) | |||
const handleChange = React.useCallback((event) => setValue(event.target.value), []) | |||
const handleSubmit = React.useCallback( | |||
(event) => { | |||
(event: React.FormEvent) => { | |||
// event.stopPropagation() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pls uncomment or remove
|
||
function resolveName( | ||
name: string, | ||
obj: Record<string, any>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this argument could probably have a more descriptive name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's here for keys only, so it doesn't matter what kind of object is here. Just some object with keys
const newName = resolveNameConflictRudely(name, attempt) | ||
if (!obj[newName]) return newName | ||
|
||
return resolveName(name, obj, attempt + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i doubt that would be an issue in practice, but in theory this can lead to error by exceeding the recursion depth (since js doesn't have a tail-call optimization, unfortunately), so if we are being pedantic, this function should be rewritten using a loop instead of recursion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean you don't want to use recursion in JS in general, or in this particular case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't "don't want" to, it's just not safe (may lead to an exception) if the depth is unbounded, and here it well may be, since we don't control what files (and how many) a user adds.
idk, maybe it's kinda ok in practice in this particular case (source):
The maximal recursion depth is limited by JavaScript engine. We can rely on it being 10000, some engines allow more, but 100000 is probably out of limit for the majority of them. There are automatic optimizations that help alleviate this (“tail calls optimizations”), but they are not yet supported everywhere and work only in simple cases.
but in general, in js recursion shouldn't be used for possibly unbounded iterative processes (tho it's ok to use it to traverse recursive data structures, ofc)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, but IMO it's an overkill to sacrifice elegance of recursion to cover edge case with 10000 duplicate names. Also, we already have a limit on number of files: 10000
@@ -86,32 +109,108 @@ export type FilesAction = tagged.InstanceOf<typeof FilesAction> | |||
|
|||
export type LocalFile = FileWithPath & FileWithHash | |||
|
|||
// TODO: queue/array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does this comment mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a list of objects instead of a single one. So, we can use infinite "revert"
...mainItems, | ||
[resolvedName]: setKeyValue( | ||
'conflict', | ||
resolvedName === path ? undefined : path, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if i read it correctly, this won't unset the prop on a File instance, because undefined is ignored (see line 160). or am i missing smth?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It creates a copy of a file, first without 'conflict' (at cloning step), and then 'conflict' is ignoring (at setting step)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't invest time in it, since in the next iteration I will not do cloning :)
) { | ||
const resolvedName = resolveName( | ||
path, | ||
itemsToCheck ? { ...mainItems, ...itemsToCheck } : mainItems, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{...undefined}
works just fine, so you can skip the check
function addFile<T extends AnyFile, M extends Record<string, AnyFile>>( | ||
path: string, | ||
file: T, | ||
mainItems: M, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do you need this type var here? you're not using it. or is it needed for the return type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I can skip it now. Earlier types were stricter, but I gave up
@@ -625,6 +624,8 @@ function PackageCreationForm({ | |||
validate={validateFiles as FF.FieldValidator<$TSFixMe>} | |||
validateFields={['files']} | |||
errors={{ | |||
conflict: | |||
'There are files with identical names. Rename or remove them please', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'There are files with identical names. Rename or remove them please', | |
'There are entries with conflicting logical keys. Rename or remove them please', |
not sure if this is more user-friendly for less-tech-savvy people, but it seems more correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if they know what logical key is, but I agree with the rest
logical_key: string | ||
size: number | ||
meta?: JsonRecord | ||
} | ||
|
||
export class EntryNameError extends BaseError { | ||
static data: ValidationEntry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't quite get it why this is needed (or at least why it's static
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is used for showing in which file there was an error. Under the FilesInput
static
is a mistyping copypasta, thanks
function resolveName( | ||
name: string, | ||
obj: Record<string, any>, | ||
attempt: number = 1, | ||
): string { | ||
if (!obj[name]) return name | ||
|
||
const newName = resolveNameConflictRudely(name, attempt) | ||
if (!obj[newName]) return newName | ||
|
||
return resolveName(name, obj, attempt + 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm not sure what sacrifice do you mean:
function resolveName( | |
name: string, | |
obj: Record<string, any>, | |
attempt: number = 1, | |
): string { | |
if (!obj[name]) return name | |
const newName = resolveNameConflictRudely(name, attempt) | |
if (!obj[newName]) return newName | |
return resolveName(name, obj, attempt + 1) | |
function resolveName(name: string, obj: Record<string, any>): string { | |
let newName = name, attempt = 1 | |
while (newName in obj) newName = resolveNameConflictRudely(name, attempt++) | |
return newName |
Is this obsoleted by @fiskus' work on Selection's last Horizon? |
No, it's still relevant. Alexei couldn't approve this due to his disagreement on tradeoffs I made in design. To reduce tradeoffs and make more suitable design for the tasks, I need to refactor this code first. Also, this unification of types can help me refactor #3681 |
conflict
is this file conflicted with some other filechanged
what was changed in this file// TODO: