-
Notifications
You must be signed in to change notification settings - Fork 61
Unsaved changes indicator #4289
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
Open
doc-han
wants to merge
31
commits into
main
Choose a base branch
from
3682-implement-unsaved-changes-indicator-red-dot-in-react
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
ae1f0cb
feat: unsaved changes
doc-han 94886b9
fix: wording
doc-han 2f4f8a8
feat: udpate
doc-han 6d98fd5
fix: resolve new workflow from templates
doc-han ee8e7e0
feat: add workflow_transform to serializer
doc-han cfcbeef
feat: remove logs
doc-han c5c40f1
feat: transform workflow
doc-han e08442c
feat: remove transforms from serializer
doc-han 43a2d19
feat: cleanup
doc-han b990b8a
fix: changes
doc-han 45f76b1
tests: update tests
doc-han af1fd4a
chore: simplify expression
doc-han dd07f94
test: resolve failing test
doc-han 1a718f7
chore: update changelog
doc-han 3497712
feat: trim all user inputs
doc-han 4d4826a
feat: support positions in changes
doc-han 1ba573d
fix: set default cron expression
doc-han 8316dcd
fix: default cron value
doc-han f48b3e1
chore: remove log
doc-han 991118f
fix: allow job credentials through encoding
doc-han dbc429c
feat: consider concurrency & jobs log toggle
doc-han b16cf24
chore: resolve linting issue
doc-han 9cf5313
feat: switch back to broadcast_from! and pass workflow in resp
doc-han 43117f3
chore: add notify call to setBaseWorkflow
doc-han 38a91ae
chore: use memoization
doc-han 5d3cab8
tests: resolve
doc-han c8d7d08
test: resolve unsaved changes test
doc-han d32599e
fix: schema validation for base workflow
doc-han 6f8b964
tests: undefined base
doc-han 7efb05c
tests: fix job logs validation
doc-han 99c0c8d
tests: broadcasts
doc-han File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
assets/js/collaborative-editor/hooks/useUnsavedChanges.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { useMemo } from 'react'; | ||
|
|
||
| import type { Trigger } from '../types/trigger'; | ||
| import type { Workflow } from '../types/workflow'; | ||
|
|
||
| import { useSessionContext } from './useSessionContext'; | ||
| import { useWorkflowState } from './useWorkflow'; | ||
|
|
||
| export function useUnsavedChanges() { | ||
| const { workflow } = useSessionContext(); | ||
|
|
||
| // Individual selectors - stable references | ||
| const jobs = useWorkflowState(state => state.jobs); | ||
| const triggers = useWorkflowState(state => state.triggers); | ||
| const edges = useWorkflowState(state => state.edges); | ||
| const positions = useWorkflowState(state => state.positions); | ||
| const name = useWorkflowState(state => state.workflow?.name); | ||
| const concurrency = useWorkflowState(state => state.workflow?.concurrency); | ||
| const enable_job_logs = useWorkflowState( | ||
| state => state.workflow?.enable_job_logs | ||
| ); | ||
|
|
||
| // Memoize store workflow object to prevent recreating on every render | ||
| const storeWorkflow = useMemo( | ||
| () => ({ | ||
| jobs, | ||
| triggers, | ||
| edges, | ||
| positions: positions || {}, | ||
| name, | ||
| concurrency, | ||
| enable_job_logs, | ||
| }), | ||
| [jobs, triggers, edges, positions, name, concurrency, enable_job_logs] | ||
| ); | ||
|
|
||
| // Memoize transformed base workflow (from session context) | ||
| const transformedBaseWorkflow = useMemo( | ||
| () => (workflow ? transformWorkflow(workflow) : null), | ||
| [workflow] | ||
| ); | ||
|
|
||
| // Memoize transformed store workflow | ||
| const transformedStoreWorkflow = useMemo( | ||
| () => transformWorkflow(storeWorkflow as Workflow), | ||
| [storeWorkflow] | ||
| ); | ||
|
|
||
| // Memoize comparison | ||
| const hasChanges = useMemo(() => { | ||
| if (!transformedBaseWorkflow) return false; | ||
| return isDiffWorkflow(transformedBaseWorkflow, transformedStoreWorkflow); | ||
| }, [transformedBaseWorkflow, transformedStoreWorkflow]); | ||
|
|
||
| return { hasChanges }; | ||
| } | ||
|
|
||
| // transform workflow to normalized structure for comparison | ||
| function transformWorkflow(workflow: Workflow) { | ||
| return { | ||
| name: workflow.name, | ||
| jobs: (workflow.jobs || []) | ||
| .map(job => ({ | ||
| id: job.id, | ||
| name: job.name.trim(), | ||
| body: job.body.trim(), | ||
| adaptor: job.adaptor, | ||
| project_credential_id: job.project_credential_id, | ||
| keychain_credential_id: job.keychain_credential_id, | ||
| })) | ||
| .sort((a, b) => a.id.localeCompare(b.id)), | ||
| edges: (workflow.edges || []) | ||
| .map(edge => ({ | ||
| id: edge.id, | ||
| source_job_id: edge.source_job_id, | ||
| source_trigger_id: edge.source_trigger_id, | ||
| target_job_id: edge.target_job_id, | ||
| enabled: edge.enabled || false, | ||
| condition_type: edge.condition_type, | ||
| condition_label: edge.condition_label?.trim(), | ||
| condition_expression: edge.condition_expression?.trim(), | ||
| })) | ||
| .sort((a, b) => a.id.localeCompare(b.id)), | ||
| triggers: (workflow.triggers || []).map(trigger => | ||
| transformTrigger(trigger) | ||
| ), | ||
| positions: workflow.positions || {}, | ||
| concurrency: workflow.concurrency, | ||
| enable_job_logs: workflow.enable_job_logs, | ||
| }; | ||
| } | ||
|
|
||
| function transformTrigger(trigger: Trigger) { | ||
| const output: Partial<Trigger> = { | ||
| id: trigger.id, | ||
| type: trigger.type, | ||
| enabled: trigger.enabled, | ||
| }; | ||
| switch (trigger.type) { | ||
| case 'cron': | ||
| output.cron_expression = trigger.cron_expression ?? '0 0 * * *'; // default cron expression | ||
| break; | ||
| case 'kafka': | ||
| output.kafka_configuration = trigger.kafka_configuration; | ||
| break; | ||
| case 'webhook': | ||
| break; | ||
| } | ||
| return output; | ||
| } | ||
|
|
||
| // deep comparison to detect workflow changes | ||
| function isDiffWorkflow(base: unknown, target: unknown): boolean { | ||
| const isNullish = (v: unknown) => v === undefined || v === null || v === ''; | ||
| if (isNullish(base) && isNullish(target)) return false; | ||
| if (typeof base !== typeof target) return true; | ||
|
|
||
| if (Array.isArray(base) && Array.isArray(target)) { | ||
| return ( | ||
| base.length !== target.length || | ||
| base.some((v, i) => isDiffWorkflow(v, target[i])) | ||
| ); | ||
| } | ||
|
|
||
| if ( | ||
| base && | ||
| target && | ||
| typeof base === 'object' && | ||
| typeof target === 'object' | ||
| ) { | ||
| const baseObj = base as Record<string, unknown>; | ||
| const targetObj = target as Record<string, unknown>; | ||
| const keys = [ | ||
| ...new Set(Object.keys(baseObj).concat(Object.keys(targetObj))), | ||
| ]; | ||
| return keys.some(k => isDiffWorkflow(baseObj[k], targetObj[k])); | ||
| } | ||
|
|
||
| return base !== target; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.