Skip to content

Commit f99db18

Browse files
Merge pull request #645 from shapehq/hotfix/preserve-scroll-on-project-refresh
Hotfix/preserve scroll on project refresh
2 parents 816579e + dc61995 commit f99db18

File tree

4 files changed

+19
-6
lines changed

4 files changed

+19
-6
lines changed

__test__/projects/GitHubProjectDataSource.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,11 +845,13 @@ test("It adds remote versions from the project configuration", async () => {
845845
id: "huey",
846846
name: "Huey",
847847
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/huey.yml" })}`,
848+
urlHash: "89ba381286214eec",
848849
isDefault: false
849850
}, {
850851
id: "dewey",
851852
name: "Dewey",
852853
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/dewey.yml" })}`,
854+
urlHash: "8f810fff152505f6",
853855
isDefault: false
854856
}]
855857
}, {
@@ -860,6 +862,7 @@ test("It adds remote versions from the project configuration", async () => {
860862
id: "louie",
861863
name: "Louie",
862864
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/louie.yml" })}`,
865+
urlHash: "b83ebf43ceede6bc",
863866
isDefault: false
864867
}]
865868
}])
@@ -925,6 +928,7 @@ test("It modifies ID of remote version if the ID already exists", async () => {
925928
id: "baz",
926929
name: "Baz",
927930
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`,
931+
urlHash: "25cb42ff63570cb5",
928932
isDefault: false
929933
}]
930934
}, {
@@ -935,6 +939,7 @@ test("It modifies ID of remote version if the ID already exists", async () => {
935939
id: "hello",
936940
name: "Hello",
937941
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/hello.yml" })}`,
942+
urlHash: "d078bd689699d1f0",
938943
isDefault: false
939944
}]
940945
}])
@@ -979,6 +984,7 @@ test("It lets users specify the ID of a remote version", async () => {
979984
id: "baz",
980985
name: "Baz",
981986
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`,
987+
urlHash: "25cb42ff63570cb5",
982988
isDefault: false
983989
}]
984990
}])
@@ -1023,6 +1029,7 @@ test("It lets users specify the ID of a remote specification", async () => {
10231029
id: "some-spec",
10241030
name: "Baz",
10251031
url: `/api/remotes/${base64RemoteConfigEncoder.encode({ url: "https://example.com/baz.yml" })}`,
1032+
urlHash: "25cb42ff63570cb5",
10261033
isDefault: false
10271034
}]
10281035
}])

src/features/projects/data/GitHubProjectDataSource.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createHash } from "crypto"
12
import { IEncryptionService } from "@/features/encrypt/EncryptionService"
23
import {
34
Project,
@@ -218,11 +219,14 @@ export default class GitHubProjectDataSource implements IProjectDataSource {
218219
};
219220

220221
const encodedRemoteConfig = this.remoteConfigEncoder.encode(remoteConfig);
222+
// 16 hex chars (64 bits) - sufficient for change detection, not cryptographic security
223+
const configHash = createHash("sha256").update(JSON.stringify(remoteConfig)).digest("hex").slice(0, 16);
221224

222225
return {
223226
id: this.makeURLSafeID((e.id || e.name).toLowerCase()),
224227
name: e.name,
225228
url: `/api/remotes/${encodedRemoteConfig}`,
229+
urlHash: configHash,
226230
isDefault: false // initial value
227231
};
228232
})

src/features/projects/domain/OpenApiSpecification.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const OpenApiSpecificationSchema = z.object({
44
id: z.string(),
55
name: z.string(),
66
url: z.string(),
7+
urlHash: z.string().optional(),
78
editURL: z.string().optional(),
89
diffURL: z.string().optional(),
910
diffBaseBranch: z.string().optional(),

src/features/projects/view/ProjectsContextProvider.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,20 @@ const ProjectsContextProvider = ({
1515
const [refreshing, setRefreshing] = useState(false);
1616
const isLoadingRef = useRef(false);
1717

18-
19-
const setProjectsAndRefreshed = (value: Project[]) => {
20-
setProjects(value);
21-
};
18+
// Fingerprint uses urlHash for remote specs (stable), URL for others (already stable)
19+
const fingerprint = (list: Project[]) =>
20+
list.flatMap(p => p.versions.flatMap(v => v.specifications.map(s => s.urlHash ?? s.url))).sort().join();
2221

2322
const refreshProjects = useCallback(() => {
2423
if (isLoadingRef.current) return;
2524
isLoadingRef.current = true;
2625
setRefreshing(true);
2726
fetch("/api/refresh-projects", { method: "POST" })
2827
.then((res) => res.json())
29-
.then(({ projects }) => {
30-
if (projects) setProjectsAndRefreshed(projects);
28+
.then(({ projects: newProjects }) => {
29+
if (newProjects) {
30+
setProjects(prev => fingerprint(prev) === fingerprint(newProjects) ? prev : newProjects);
31+
}
3132
})
3233
.catch((error) => console.error("Failed to refresh projects", error))
3334
.finally(() => {

0 commit comments

Comments
 (0)