Skip to content

Conversation

Saksham-Sirohi
Copy link
Contributor

@Saksham-Sirohi Saksham-Sirohi commented Jun 18, 2025

Migration Fixes part of #359

Summary by Sourcery

Migrate schedule-editor to Vue 3 and Composition API, update list rendering keys, and modernize build and lint configuration

Enhancements:

  • Convert all components (GridSchedule, Session, Editor, App) from the Vue 2 Options API to Vue 3 using <script setup> and Composition API primitives
  • Add explicit :key attributes to all v-for loops for consistent list rendering
  • Refactor main initialization to asynchronously mount app with i18n plugin
  • Simplify component logic via defineProps, defineEmits, refs, and computed properties

Build:

  • Update vite.config.js with new aliases for moment and add preview script
  • Bump package.json dependencies and devDependencies to latest compatible versions
  • Replace legacy .eslintrc/.stylelintrc files with a unified eslint.config.js and update lint scripts

Summary by Sourcery

Migrate the schedule-editor codebase from Vue 2 to Vue 3 with full TypeScript and Composition API support, add explicit keys for all list renderings, introduce type-safe API schemas via zod, and modernize the build, lint and config setup.

Enhancements:

  • Rewrite core components (GridSchedule, App, Session) in Vue 3 using <script setup> and Composition API with TypeScript
  • Add explicit :key attributes to all v-for loops to ensure stable list rendering
  • Introduce a new api.ts with zod schemas for type-safe API interactions and data validation

Build:

  • Convert main entry to TypeScript (main.ts) and update vite.config.js with TS support, aliases, manual chunks and preview script
  • Replace legacy eslint/stylelint configs with a unified eslint.config.js, add lint:tsc script and update package.json scripts
  • Add tsconfig.json, module declaration files, and bump dependencies/devDependencies to latest Vue 3, TypeScript, zod, and related plugins

Copy link

sourcery-ai bot commented Jun 18, 2025

Reviewer's Guide

This PR migrates the schedule-editor to Vue 3 by rewriting all components with the Composition API and TypeScript, ensures stable list rendering with explicit keys, refactors app initialization to asynchronously mount with an i18n plugin, and modernizes the build and lint setup—including adding a type-safe API layer with Zod.

File-Level Changes

Change Details Files
Rewrite components using Vue 3 Composition API and TypeScript
  • Convert <script> blocks to <script setup lang="ts"> with defineProps/defineEmits
  • Replace data/computed/methods/watchers with refs, computed, and lifecycle hooks
  • Define and use TS interfaces for props, sessions, rooms, timeslices
src/pretalx/frontend/schedule-editor/src/components/GridSchedule.vue
src/pretalx/frontend/schedule-editor/src/components/Session.vue
src/pretalx/frontend/schedule-editor/src/App.vue
Add explicit v-for keys for deterministic list rendering
  • Add :key bindings on all v-for loops (timeslices, rooms, sessions, days, unscheduled)
  • Replace implicit ref attributes with a setTimesliceRef callback for dynamic refs
src/pretalx/frontend/schedule-editor/src/components/GridSchedule.vue
src/pretalx/frontend/schedule-editor/src/App.vue
Refactor app bootstrap to asynchronous i18n mount
  • Convert main.js to main.ts and import the i18n plugin
  • Await loading of locale files before mounting the app
src/pretalx/frontend/schedule-editor/src/main.ts
src/pretalx/frontend/schedule-editor/src/lib/i18n.ts
Modernize build and lint configuration
  • Update vite.config.js with TypeScript support, new aliases, and preview script
  • Bump dependencies/devDependencies and add lint:tsc, preview and build scripts in package.json
  • Replace legacy ESLint/Stylelint files with a unified eslint.config.js and add tsconfig.json and modules.d.ts
src/pretalx/frontend/schedule-editor/vite.config.js
src/pretalx/frontend/schedule-editor/package.json
src/pretalx/frontend/schedule-editor/eslint.config.js
src/pretalx/frontend/schedule-editor/tsconfig.json
src/pretalx/frontend/schedule-editor/src/modules.d.ts
Introduce type-safe API layer with Zod schemas
  • Create api.ts implementing HTTP methods validated by Zod schemas
  • Decommission the old api.js and inline schema parsing for talks, availabilities, warnings
src/pretalx/frontend/schedule-editor/src/api.ts

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Saksham-Sirohi Saksham-Sirohi marked this pull request as draft June 18, 2025 18:26
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Saksham-Sirohi - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/pretalx/frontend/schedule-editor/src/main.js:13` </location>
<code_context>
-app.use(await i18n(lang))
-app.mount('#app')
+const lang = document.querySelector("#app").dataset.gettext;
+(async () => {
+    const i18nPlugin = await i18n(lang)
+    app.use(i18nPlugin)
+    app.mount('#app')
+})()
\ No newline at end of file
</code_context>

<issue_to_address>
Wrapping app initialization in an async IIFE may affect error handling.

Add error handling to the async IIFE to ensure initialization errors are logged or shown to the user.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Saksham-Sirohi Saksham-Sirohi marked this pull request as ready for review June 21, 2025 18:21
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Saksham-Sirohi - I've reviewed your changes - here's some feedback:

  • In the availability v-for you’re using the array index as a key; consider switching to a stable unique identifier (e.g. a timestamp or ID) to avoid rendering glitches when the list changes.
  • You’ve enabled the vue/component-api-style rule targeting <script setup>, but all components still use the Options API—consider migrating to <script setup> for consistency with your linting config.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In the availability v-for you’re using the array index as a key; consider switching to a stable unique identifier (e.g. a timestamp or ID) to avoid rendering glitches when the list changes.
- You’ve enabled the `vue/component-api-style` rule targeting `<script setup>`, but all components still use the Options API—consider migrating to `<script setup>` for consistency with your linting config.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Saksham-Sirohi Saksham-Sirohi self-assigned this Jun 21, 2025
@Saksham-Sirohi Saksham-Sirohi force-pushed the vue-3-migration branch 2 times, most recently from 103a13c to 21dce88 Compare June 22, 2025 06:14
@Saksham-Sirohi Saksham-Sirohi marked this pull request as draft June 22, 2025 16:07
@Saksham-Sirohi Saksham-Sirohi force-pushed the vue-3-migration branch 2 times, most recently from 74824b2 to 624b14b Compare June 22, 2025 20:02
@Saksham-Sirohi Saksham-Sirohi marked this pull request as ready for review June 22, 2025 20:36
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Saksham-Sirohi - I've reviewed your changes - here's some feedback:

  • GridSchedule.vue has become very large—consider extracting the timeslice generation and drag/scroll logic into separate composables or helper modules to improve readability and maintainability.
  • You never clean up the IntersectionObserver created in onMounted; use onUnmounted to disconnect it and avoid potential memory leaks.
  • In a few v-for loops (e.g. availabilitySlices) you’re still using the array index as key—swap to a stable unique identifier if possible to prevent rendering glitches when the list changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- GridSchedule.vue has become very large—consider extracting the timeslice generation and drag/scroll logic into separate composables or helper modules to improve readability and maintainability.
- You never clean up the IntersectionObserver created in onMounted; use onUnmounted to disconnect it and avoid potential memory leaks.
- In a few v-for loops (e.g. availabilitySlices) you’re still using the array index as key—swap to a stable unique identifier if possible to prevent rendering glitches when the list changes.

## Individual Comments

### Comment 1
<location> `src/pretalx/frontend/schedule-editor/src/App.vue:359` </location>
<code_context>
+}
+
+function startDragging({event, session}) {
+  if (availabilities.value && availabilities.value.talks[session.id] && availabilities.value.talks[session.id].length !== 0) {
+    session.availabilities = availabilities.value.talks[session.id]
+  }
</code_context>

<issue_to_address>
Directly mutating session.availabilities may break reactivity.

If session is a prop or part of a reactive source, use a reactive update method instead of direct assignment to ensure changes are tracked.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Saksham-Sirohi Saksham-Sirohi force-pushed the vue-3-migration branch 2 times, most recently from 176b616 to d84dd1b Compare June 22, 2025 21:25
@Saksham-Sirohi
Copy link
Contributor Author

@sourcery-ai review

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Saksham-Sirohi - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/pretalx/frontend/schedule-editor/src/components/Session.vue:59` </location>
<code_context>
+	}
+})
+
+const link = computed(() => {
+	return generateSessionLinkUrl({eventUrl: eventUrl.value, session: props.session})
+})
+
+const isBreak = computed(() => !props.session.code)
</code_context>

<issue_to_address>
Potential undefined access for eventUrl in generateSessionLinkUrl.

'eventUrl' is injected as a plain value, not a ref, so accessing 'eventUrl.value' may be undefined. Use 'eventUrl' directly if it's not a ref, or ensure it is always a ref for consistency.
</issue_to_address>

### Comment 2
<location> `src/pretalx/frontend/schedule-editor/src/App.vue:111` </location>
<code_context>
+const unassignedSortDirection = ref(1)  // asc
+const showUnassignedSortMenu = ref(false)
+const newBreakTooltip = ref('')
+const translations = ref({
+  filterSessions: inject('$t')('Filter sessions'),
+  newBreak: inject('$t')('New break'),
+})
+
+const $t = inject('$t')
</code_context>

<issue_to_address>
translations ref is initialized before $t is guaranteed to be available.

Initializing 'translations' with '$t' before it is injected may cause issues if i18n is not yet available. Initialize 'translations' after '$t' is set, or use a computed property to ensure proper timing.
</issue_to_address>

### Comment 3
<location> `src/pretalx/frontend/schedule-editor/src/App.vue:234` </location>
<code_context>
+})
+
+const dateFormat = computed(() => {
+  if ((schedule.value && schedule.value.rooms.length > 2) || !days.value || !days.value.length) return 'dddd DD. MMMM'
+  if (days.value && days.value.length <= 5) return 'dddd DD. MMMM'
+  if (days.value && days.value.length <= 7) return 'dddd DD. MMM'
</code_context>

<issue_to_address>
dateFormat computed property may return undefined if none of the conditions match.

If days.value has more than 7 elements, dateFormat will return undefined. Please add a default return value to handle this case.
</issue_to_address>

### Comment 4
<location> `src/pretalx/frontend/schedule-editor/src/App.vue:254` </location>
<code_context>
+})
+
+async function initApp() {
+  try {
+    schedule.value = await fetchSchedule()
+    eventTimezone = schedule.value.timezone
+    moment.tz.setDefault(eventTimezone)
+    locales.value = schedule.value.locales
+    eventSlug.value = window.location.pathname.split("/")[3]
+    
+    await nextTick()
+    if (days.value && days.value.length) {
+      currentDay.value = days.value[0]
+    }
+    
+    setTimeout(pollUpdates, 10 * 1000)
</code_context>

<issue_to_address>
Error handling in pollUpdates may cause silent failures.

Consider implementing a maximum retry limit or exponential backoff in 'pollUpdates' to prevent endless error-retry cycles if persistent errors occur.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    setTimeout(pollUpdates, 10 * 1000)
    await fetchAdditionalScheduleData()
  } catch (error) {
    console.error('Failed to initialize app:', error)
  }
}
=======
    setTimeout(() => pollUpdates(), 10 * 1000)
    await fetchAdditionalScheduleData()
  } catch (error) {
    console.error('Failed to initialize app:', error)
  }
}

// Polling state for retries and backoff
let pollRetryCount = 0
const POLL_MAX_RETRIES = 5
const POLL_INITIAL_DELAY = 10 * 1000 // 10 seconds
const POLL_MAX_DELAY = 5 * 60 * 1000 // 5 minutes

function getPollDelay() {
  // Exponential backoff: min(initial * 2^retries, max)
  return Math.min(POLL_INITIAL_DELAY * Math.pow(2, pollRetryCount), POLL_MAX_DELAY)
}

async function pollUpdates() {
  try {
    // ... your polling logic here ...
    // On success, reset retry state:
    pollRetryCount = 0
    setTimeout(() => pollUpdates(), POLL_INITIAL_DELAY)
  } catch (error) {
    pollRetryCount += 1
    if (pollRetryCount > POLL_MAX_RETRIES) {
      console.error('Polling failed after maximum retries:', error)
      // Optionally, notify the user or stop polling
      return
    }
    const delay = getPollDelay()
    console.warn(`Polling failed (attempt ${pollRetryCount}), retrying in ${delay / 1000}s`, error)
    setTimeout(() => pollUpdates(), delay)
  }
}
>>>>>>> REPLACE

</suggested_fix>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@mariobehling mariobehling changed the title migration: schedule-editor-migration Migration: schedule-editor-migration to Vue 3 Jun 24, 2025
@mariobehling
Copy link
Member

Please resolve conflicts and add screenshots of functioning system.

@Saksham-Sirohi Saksham-Sirohi marked this pull request as draft June 29, 2025 21:09
@Saksham-Sirohi Saksham-Sirohi force-pushed the vue-3-migration branch 3 times, most recently from 863c0e9 to 3803140 Compare July 3, 2025 16:47
@Saksham-Sirohi Saksham-Sirohi marked this pull request as ready for review July 3, 2025 17:05
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Saksham-Sirohi - I've reviewed your changes - here's some feedback:

  • Consider fully adopting <script setup> in your Vue 3 components to streamline prop/emits definitions and remove the boilerplate defineComponent calls.
  • Add cleanup logic in the unmounted hook to disconnect your IntersectionObserver and avoid potential memory leaks after the component is destroyed.
  • Use the v-for index parameter for computing grid‐area keys (e.g. v-for="(room, i) in visibleRooms" :key="room.id" :style="{'grid-area': 1 / ${i+2} / auto / auto}") instead of calling visibleRooms.indexOf at render time for more stable keys and better performance.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider fully adopting <script setup> in your Vue 3 components to streamline prop/emits definitions and remove the boilerplate defineComponent calls.
- Add cleanup logic in the unmounted hook to disconnect your IntersectionObserver and avoid potential memory leaks after the component is destroyed.
- Use the v-for index parameter for computing grid‐area keys (e.g. `v-for="(room, i) in visibleRooms" :key="room.id" :style="{'grid-area': `1 / ${i+2} / auto / auto`}"`) instead of calling visibleRooms.indexOf at render time for more stable keys and better performance.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Saksham-Sirohi Saksham-Sirohi force-pushed the vue-3-migration branch 2 times, most recently from bbdaebf to bfce941 Compare July 15, 2025 18:24
@Saksham-Sirohi
Copy link
Contributor Author

Saksham-Sirohi commented Jul 16, 2025

image Working updated system with composition api using script setup

@Saksham-Sirohi Saksham-Sirohi marked this pull request as draft July 25, 2025 11:30
Copy link
Member

@hongquan hongquan left a comment

Choose a reason for hiding this comment

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

Btw, you must validate API response, or any incoming data from external.
Use Zod for it.

I don't have a public code with Zod to show you as example, but you take a look at the 2nd part of this article: https://quan.hoabinh.vn/post/2025/4/do-choi-de-thay-graphql-do-phien (it is Vietnamese, though).

The article show example with Valibot and GraphQL, but you can apply the same with Zod and RESTful API.
Zod API is simpler than Valibot.

})
},

getList(url: string): Promise<any[]> {
Copy link
Member

@hongquan hongquan Jul 25, 2025

Choose a reason for hiding this comment

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

Using any type beats the purpose of using TypeScript (it is like using pure JavaScript).

Why, assume that you have some code:

const talks = await getList()
talks.fly()

TypeScript can not help us check if .fly() is valid or not, because you denote talks as any, it can accept any property and method.

@hongquan
Copy link
Member

Try to make the return type narrow.

For example, in this code:

const durationPretty = computed<string | undefined>(() => {
  const minutes = durationMinutes.value
  if (!minutes) return undefined

Making type string | undefined is unecessary, because empty string can be used for "no data". Use just string type.

@Saksham-Sirohi Saksham-Sirohi marked this pull request as ready for review July 29, 2025 13:51
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Saksham-Sirohi - I've reviewed your changes - here's some feedback:

  • The huge computed blocks (especially the timeslices logic) could be extracted into standalone composables or utility modules to improve readability and make unit testing easier.
  • I see repeated date/grid‐area formatting logic (e.g. getSliceName and grid area strings)—consider centralizing these in a shared helper to DRY up the code across components.
  • Your api.ts file is very large and mixes Zod schemas with HTTP logic—splitting schemas and request functions into separate modules or folders would help keep each file focused and maintainable.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The huge computed blocks (especially the timeslices logic) could be extracted into standalone composables or utility modules to improve readability and make unit testing easier.
- I see repeated date/grid‐area formatting logic (e.g. getSliceName and grid area strings)—consider centralizing these in a shared helper to DRY up the code across components.
- Your api.ts file is very large and mixes Zod schemas with HTTP logic—splitting schemas and request functions into separate modules or folders would help keep each file focused and maintainable.

## Individual Comments

### Comment 1
<location> `src/pretalx/frontend/schedule-editor/src/components/GridSchedule.vue:402` </location>
<code_context>
+  return result
+})
+
+const setTimesliceRef = (el: HTMLElement | null) => {
+  if (el && !timesliceRefs.value.includes(el)) timesliceRefs.value.push(el)
+}
</code_context>

<issue_to_address>
Potential memory leak: timesliceRefs array is never cleared.

Since timesliceRefs is only appended to and never cleared, this may cause memory leaks or stale references if the component re-renders or timeslices change. Reset or update the array as needed to prevent this.
</issue_to_address>

### Comment 2
<location> `src/pretalx/frontend/schedule-editor/src/App.vue:522` </location>
<code_context>
+    schedule.value.talks = updatedTalks
+  }
+  since.value = sched.now || schedule.value.now
+  window.setTimeout(pollUpdates, 10 * 30)
+}
+
</code_context>

<issue_to_address>
Polling interval is set to 300ms, which may be too frequent.

This change increases polling frequency from every 10s to every 300ms, which may lead to unnecessary network load and performance degradation. Please consider reverting to the previous interval.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Saksham-Sirohi Saksham-Sirohi requested a review from hongquan July 31, 2025 17:57
@hongquan
Copy link
Member

hongquan commented Aug 1, 2025

This code is very bad:

return undefined as unknown as T;

How come undefined can be treated as a concete type?

@hongquan
Copy link
Member

hongquan commented Aug 1, 2025

  async createTalk(talk: Omit<TalkPayload, 'id'>): Promise<z.infer<typeof TalkSchema>> {
    const response = await this.saveTalk(talk, { action: 'POST' }) as z.infer<typeof TalkSchema>;
    return TalkSchema.parse(response);
  }

Why repeating z.infer<typeof TalkSchema> so many times. Let's define:

export type Talk = z.infer<typeof TalkSchema>

and reuse it.

Put the Zod schemas and type definitions (from z.infer) to a schemas or models module.

Using too many type cast (as) is bad. It is a sign of bad design.
For example:

await this.saveTalk(talk, { action: 'POST' }) as z.infer<typeof TalkSchema>

You should define saveTalk so that it returns a value of Talk type. By doing so, when we use:

const response = await this.saveTalk(talk)

the response will have Talk type, without casting.

@Saksham-Sirohi Saksham-Sirohi force-pushed the vue-3-migration branch 2 times, most recently from 401ec92 to b58cc83 Compare August 6, 2025 15:38
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.

3 participants