Skip to content

Commit 8bc70e5

Browse files
committed
feat: working commit - need review
1 parent eed6731 commit 8bc70e5

22 files changed

Lines changed: 2220 additions & 486 deletions

packages/imagekit-editor-dev/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "imagekit-editor-dev",
3-
"version": "2.2.0",
3+
"version": "2.2.0-dev-13-04-2026.2",
44
"description": "AI Image Editor powered by ImageKit",
55
"scripts": {
66
"prepack": "yarn build",
@@ -27,6 +27,9 @@
2727
"devDependencies": {
2828
"@emotion/babel-plugin": "^11.13.5",
2929
"@microsoft/api-extractor": "7.34.9",
30+
"@testing-library/dom": "8.20.1",
31+
"@testing-library/jest-dom": "^6.9.1",
32+
"@testing-library/react": "12.1.5",
3033
"@types/lodash": "^4",
3134
"@types/node": "^20.11.24",
3235
"@types/react": "^17.0.2",
@@ -35,6 +38,7 @@
3538
"@vitejs/plugin-react": "^4.5.2",
3639
"@vitest/coverage-v8": "^2.1.9",
3740
"@vitest/ui": "^2.0.0",
41+
"happy-dom": "^20.9.0",
3842
"react": "^17.0.2",
3943
"react-dom": "^17.0.2",
4044
"rollup-plugin-visualizer": "^5.12.0",

packages/imagekit-editor-dev/src/ImageKitEditor.tsx

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -112,35 +112,49 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
112112
)
113113

114114
const saveTemplateImperative = useCallback(async () => {
115+
// Avoid importing hooks here; implement via store+provider with version gating.
115116
if (!resolvedProvider) return
116-
const s = useEditorStore.getState()
117-
if (s.templateStorageWriteBlocked) return
118-
119-
const {
120-
setSyncStatus,
121-
setTemplateId,
122-
setTemplateName,
123-
denyTemplateStorageAccess,
124-
} = s
125-
setSyncStatus("saving")
117+
const state = useEditorStore.getState()
118+
if (state.templateStorageWriteBlocked) return
119+
120+
const saveStartedAtVersion = state.localChangeVersion
121+
state.setSyncStatus("saving")
126122
try {
127123
const saved = await resolvedProvider.saveTemplate({
128-
id: s.templateId ?? undefined,
129-
name: s.templateName,
130-
transformations: s.transformations.map(({ id: _id, ...rest }) => rest),
124+
id: state.templateId ?? undefined,
125+
name: state.templateName,
126+
transformations: state.transformations.map(
127+
({ id: _id, ...rest }) => rest,
128+
),
129+
...(state.templateIsPrivate !== null
130+
? { isPrivate: state.templateIsPrivate }
131+
: {}),
131132
})
132-
setTemplateId(saved.id)
133-
setTemplateName(saved.name)
134-
setSyncStatus("saved")
133+
const after = useEditorStore.getState()
134+
after.hydrateTemplateMetadata({
135+
templateId: saved.id,
136+
templateName: saved.name,
137+
templateIsPrivate:
138+
typeof saved.isPrivate === "boolean" ? saved.isPrivate : null,
139+
})
140+
if (after.localChangeVersion === saveStartedAtVersion) {
141+
after.markSynced(saveStartedAtVersion)
142+
after.setSyncStatus("saved")
143+
} else {
144+
after.setSyncStatus("unsaved")
145+
}
146+
after.setLastSavedAt(Date.now())
135147
} catch (err) {
148+
const { denyTemplateStorageAccess } = useEditorStore.getState()
149+
// Reuse existing access-denied mapping.
136150
if (
137151
applyTemplateStorageAccessFailure(err, {
138152
denyTemplateStorageAccess,
139153
})
140154
) {
141155
return
142156
}
143-
setSyncStatus(
157+
state.setSyncStatus(
144158
"error",
145159
err instanceof Error ? err.message : "Failed to save template",
146160
)
@@ -151,10 +165,11 @@ function ImageKitEditorImpl<M extends RequiredMetadata>(
151165
// `dirty` should represent *unsynced* changes (host uses it to decide
152166
// whether to show a close confirmation).
153167
const state = useEditorStore.getState()
154-
const hasChanges = !state.isPristine
155-
const dirty = resolvedProvider
156-
? hasChanges && state.syncStatus !== "saved"
157-
: hasChanges
168+
const dirty =
169+
state.transformationConfigFormDirty ||
170+
(resolvedProvider
171+
? state.localChangeVersion !== state.lastSyncedVersion
172+
: !state.isPristine)
158173
props.onClose({ dirty, destroy })
159174
}
160175

packages/imagekit-editor-dev/src/components/header/SettingsModal.tsx

Lines changed: 54 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ import { PiGlobe } from "@react-icons/all-files/pi/PiGlobe"
1313
import { PiLock } from "@react-icons/all-files/pi/PiLock"
1414
import { PiTrash } from "@react-icons/all-files/pi/PiTrash"
1515
import { PiX } from "@react-icons/all-files/pi/PiX"
16-
import { useEffect, useRef, useState } from "react"
17-
import Select from "react-select"
16+
import type React from "react"
17+
import { useEffect, useMemo, useRef, useState } from "react"
18+
import Select, { type StylesConfig } from "react-select"
1819
import { useTemplateStorage } from "../../context/TemplateStorageContext"
20+
import { useTemplateSync } from "../../hooks/useTemplateSync"
1921
import { applyTemplateStorageAccessFailure } from "../../storage/templateAccessError"
2022
import { useEditorStore } from "../../store"
2123

24+
const FlexAny = Flex as unknown as React.FC<Record<string, unknown>>
25+
2226
function visibilityFromKnownPrivate(
2327
isPrivate: boolean | null,
2428
): "everyone" | "onlyMe" {
@@ -38,17 +42,15 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
3842
const provider = useTemplateStorage()
3943
const templateId = useEditorStore((s) => s.templateId)
4044
const templateName = useEditorStore((s) => s.templateName)
41-
const setTemplateName = useEditorStore((s) => s.setTemplateName)
42-
const setTemplateId = useEditorStore((s) => s.setTemplateId)
43-
const transformations = useEditorStore((s) => s.transformations)
44-
const setSyncStatus = useEditorStore((s) => s.setSyncStatus)
45+
const setTemplateIsPrivate = useEditorStore((s) => s.setTemplateIsPrivate)
4546
const resetToNewTemplate = useEditorStore((s) => s.resetToNewTemplate)
4647
const denyTemplateStorageAccess = useEditorStore(
4748
(s) => s.denyTemplateStorageAccess,
4849
)
4950
const templateStorageWriteBlocked = useEditorStore(
5051
(s) => s.templateStorageWriteBlocked,
5152
)
53+
const { saveNow } = useTemplateSync()
5254

5355
// Stable ref so the getTemplate effect doesn't re-run when onClose identity changes.
5456
const onCloseRef = useRef(onClose)
@@ -63,7 +65,6 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
6365
const [canChangeVisibility, setCanChangeVisibility] = useState(true)
6466
const [isDeleting, setIsDeleting] = useState(false)
6567
const [isSaving, setIsSaving] = useState(false)
66-
const prevVisibilityRef = useRef(localVisibility)
6768

6869
useEffect(() => {
6970
setLocalName(templateName)
@@ -92,13 +93,10 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
9293
return
9394
}
9495
setLocalVisibility(record.isPrivate ? "onlyMe" : "everyone")
95-
console.log(
96-
provider?.getCurrentUserSession(),
97-
record.createdBy.userId === provider?.getCurrentUserSession()?.id,
98-
)
99-
setCanChangeVisibility(
100-
record.createdBy.userId === provider?.getCurrentUserSession()?.id,
101-
)
96+
const session = provider.getCurrentUserSession() as {
97+
id?: string
98+
} | null
99+
setCanChangeVisibility(record.createdBy.userId === session?.id)
102100
})
103101
.catch((err) => {
104102
if (
@@ -120,20 +118,21 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
120118
const saveTemplate = async (opts?: { closeAfter?: boolean }) => {
121119
if (!provider || !localName.trim() || templateStorageWriteBlocked) return
122120

123-
setIsSaving(true)
124-
setSyncStatus("saving")
125-
126121
try {
127-
const saved = await provider.saveTemplate({
128-
id: templateId ?? undefined,
129-
name: localName.trim(),
130-
transformations: transformations.map(({ id: _id, ...rest }) => rest),
131-
isPrivate: localVisibility === "onlyMe",
122+
setIsSaving(true)
123+
const saved = await saveNow({
124+
reason: "settings",
125+
overrides: {
126+
name: localName.trim(),
127+
isPrivate: localVisibility === "onlyMe",
128+
},
129+
})
130+
if (!saved) return
131+
useEditorStore.getState().hydrateTemplateMetadata({
132+
templateId: saved.id,
133+
templateName: localName.trim(),
134+
templateIsPrivate: saved.isPrivate,
132135
})
133-
134-
setTemplateId(saved.id)
135-
setTemplateName(localName.trim())
136-
setSyncStatus("saved")
137136
if (opts?.closeAfter !== false) {
138137
onClose()
139138
}
@@ -148,29 +147,14 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
148147
}
149148
return
150149
}
151-
setSyncStatus(
152-
"error",
153-
err instanceof Error ? err.message : "Failed to save",
154-
)
155150
} finally {
156151
setIsSaving(false)
157152
}
158153
}
159154

160-
// Auto-save visibility changes (instant, like template name).
161-
useEffect(() => {
162-
if (!provider || !templateId) return
163-
if (!canChangeVisibility) return
164-
if (isSaving || isDeleting || templateStorageWriteBlocked) return
165-
const prev = prevVisibilityRef.current
166-
if (prev === localVisibility) return
167-
prevVisibilityRef.current = localVisibility
168-
void saveTemplate({ closeAfter: false })
169-
// eslint-disable-next-line react-hooks/exhaustive-deps
170-
}, [localVisibility])
171-
172155
const handleDelete = async () => {
173156
if (!provider || !templateId) return
157+
if (!provider.deleteTemplate) return
174158

175159
setIsDeleting(true)
176160

@@ -207,6 +191,30 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
207191
}
208192
}, [onClose])
209193

194+
const selectStyles = useMemo<
195+
StylesConfig<{ value: string; label: string }, false>
196+
>(
197+
() => ({
198+
control: (base) => ({
199+
...base,
200+
fontSize: "12px",
201+
minHeight: "32px",
202+
borderColor: "#E2E8F0",
203+
backgroundColor: canChangeVisibility ? base.backgroundColor : "#F7FAFC",
204+
opacity: canChangeVisibility ? 1 : 0.6,
205+
}),
206+
menu: (base) => ({
207+
...base,
208+
zIndex: 10,
209+
}),
210+
option: (base) => ({
211+
...base,
212+
fontSize: "12px",
213+
}),
214+
}),
215+
[canChangeVisibility],
216+
)
217+
210218
return (
211219
<Box
212220
position="fixed"
@@ -232,7 +240,7 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
232240
onClick={(e) => e.stopPropagation()}
233241
>
234242
{/* Header */}
235-
<Flex
243+
<FlexAny
236244
px="6"
237245
py="4"
238246
alignItems="center"
@@ -250,7 +258,7 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
250258
icon={<Icon as={PiX} boxSize={5} />}
251259
aria-label="Close settings"
252260
/>
253-
</Flex>
261+
</FlexAny>
254262

255263
{/* Content */}
256264
<Box px="6" py="6" flex="1" overflowY="auto">
@@ -296,6 +304,7 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
296304
if (!canChangeVisibility) return
297305
if (option) {
298306
setLocalVisibility(option.value as "everyone" | "onlyMe")
307+
setTemplateIsPrivate(option.value === "onlyMe")
299308
}
300309
}}
301310
options={[
@@ -312,26 +321,7 @@ export function SettingsModal({ onClose, knownIsPrivate }: SettingsModalProps) {
312321
<span>{data.label}</span>
313322
</Box>
314323
)}
315-
styles={{
316-
control: (base) => ({
317-
...base,
318-
fontSize: "12px",
319-
minHeight: "32px",
320-
borderColor: "#E2E8F0",
321-
backgroundColor: canChangeVisibility
322-
? base.backgroundColor
323-
: "#F7FAFC",
324-
opacity: canChangeVisibility ? 1 : 0.6,
325-
}),
326-
menu: (base) => ({
327-
...base,
328-
zIndex: 10,
329-
}),
330-
option: (base) => ({
331-
...base,
332-
fontSize: "12px",
333-
}),
334-
}}
324+
styles={selectStyles}
335325
isSearchable={false}
336326
isDisabled={!canChangeVisibility}
337327
/>

0 commit comments

Comments
 (0)