Skip to content
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

Migrate to Planner State & Syncing #346

Draft
wants to merge 42 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3f1342f
feat(web): #135 WIP
Maelstromeous Jan 11, 2025
0ffab35
feat(web): WIP
Maelstromeous Jan 11, 2025
7aa56c9
feat(backend): Add new PlannerState model and changed backend code to…
Maelstromeous Jan 13, 2025
a6e496a
feat(backend): WIP tab syncing
Maelstromeous Jan 13, 2025
8eb43fe
feat(web): First pass at creating a PlannerState system, where all ta…
Maelstromeous Jan 13, 2025
2df3847
Merge branch 'refs/heads/main' into 135-all-tab-sync
Maelstromeous Jan 13, 2025
601614f
feat(web): Deleting tabs working
Maelstromeous Jan 13, 2025
bac6630
feat(web): Swapping tabs working
Maelstromeous Jan 13, 2025
7a2ce83
feat(web): Merge issue
Maelstromeous Jan 13, 2025
1a283f7
feat(web): Tested migration from old data to new
Maelstromeous Jan 13, 2025
596aff6
feat(web): Moved migration code to a planner state manager, added tests
Maelstromeous Jan 13, 2025
2322b41
feat(web): Added tests for plannerStateManagement
Maelstromeous Jan 13, 2025
ed68fd3
feat(web): Slowly loosing my mind over references...
Maelstromeous Jan 13, 2025
eee51e6
feat(web): Now swaps tabs without deleting data but there's still mas…
Maelstromeous Jan 14, 2025
65f9b88
fix(web): Remove debugging
Maelstromeous Jan 14, 2025
5d054ff
fix(web): Fix sync tests
Maelstromeous Jan 14, 2025
211da7b
fix(web): Fix sharing button
Maelstromeous Jan 14, 2025
55240f8
fix(web): Fix share page
Maelstromeous Jan 14, 2025
c8364d3
fix(web): Fix share page and sync store
Maelstromeous Jan 14, 2025
8228c44
debug(web): Add debug
Maelstromeous Jan 14, 2025
37e3851
feat(web): Avoid recalculating factories when reordering
Maelstromeous Jan 14, 2025
3466eee
feat(web): Force recalculation when loading from template, otherwise …
Maelstromeous Jan 14, 2025
05ef4c0
feat(web): Make edge warning wait longer, needs replacing with a user…
Maelstromeous Jan 14, 2025
5305dcc
feat(web): Added proper handling of deleting and adding tabs keeping …
Maelstromeous Jan 14, 2025
60ba007
feat(web): Added tracking of edits, we were in a reactivity storm. Ad…
Maelstromeous Jan 15, 2025
6354b4c
feat(web): Change how user options are referenced
Maelstromeous Jan 15, 2025
1e5b036
fix(web): Fixed recursive loop when planner is empty
Maelstromeous Jan 17, 2025
81d1689
Merge branch 'main' into 135-all-tab-sync
Maelstromeous Jan 17, 2025
950e27e
Merge branch 'main' into 135-all-tab-sync
Maelstromeous Jan 18, 2025
a63e3ee
feat(web): Removed /needsmigration endpoint, can be handled on frontend
Maelstromeous Jan 18, 2025
e81647b
feat(web): Changed force loading
Maelstromeous Jan 18, 2025
7ae3211
Merge branch 'main' into 135-all-tab-sync
Maelstromeous Jan 18, 2025
adcd245
fix(web): Fixed build
Maelstromeous Jan 18, 2025
149fbd4
fix(web): Lintfix
Maelstromeous Jan 18, 2025
d6fead9
fix(web): Fixed test
Maelstromeous Jan 18, 2025
840dd7a
refactor(web): Refactored how introduction is shown and closed
Maelstromeous Jan 18, 2025
bfe8292
refactor(web): Console tweak
Maelstromeous Jan 18, 2025
9b3ac57
feat(web): Reverted back to using local storage to track latest edits…
Maelstromeous Jan 18, 2025
57ea76d
feat(web): Fixed build
Maelstromeous Jan 18, 2025
061326d
Merge branch 'main' into 135-all-tab-sync
Maelstromeous Jan 19, 2025
a7ff24e
feat(web): Conquered the beast that is Vue's reactivity system. Conve…
Maelstromeous Jan 19, 2025
7afe3a9
feat(web): Fix app store and planer state management tests
Maelstromeous Jan 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 48 additions & 28 deletions backend/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
import { generateSlug } from "random-word-slugs";

import {FactoryData} from "./models/FactoyDataSchema";
import {PlannerState as PlannerStateSchema} from "./models/PlannerState";
import {User} from "./models/UsersSchema";
import {Share, ShareDataSchema} from "./models/ShareSchema";
import {Factory} from "./interfaces/FactoryInterface";
import {Factory, FactoryTab, PlannerState} from "./interfaces/FactoryInterface";

dotenv.config();

Expand Down Expand Up @@ -195,49 +196,68 @@
});

// Save Data Endpoint
app.post('/save', authenticate, async (req: AuthenticatedRequest & TypedRequestBody<{ data: any }>, res: Express.Response) => {

Check warning on line 199 in backend/backend.ts

View workflow job for this annotation

GitHub Actions / Build Backend

Unexpected any. Specify a different type
try {
const { username } = req.user as jwt.JwtPayload & { username: string };
const factoryData: Factory[] = req.body;
const plannerState: PlannerState = req.body;

const tabs = plannerState.tabs;

// Limit the number of tabs to prevent abuse
if (tabs.length > 25) {
console.warn(`User ${username} tried to save a planner state with too many tabs!`);
plannerState.tabs = tabs.slice(0, 25);
}

// Check users are not doing naughty things with the notes and task fields
factoryData.forEach((factory) => {
if (factory.name.length > 200) {
console.warn(`User ${username} tried to save a factory name that was too long!`);
factory.name = factory.name.substring(0, 200);
tabs.forEach((tab: FactoryTab) => {
// Limit the number of factories in a tab to prevent abuse
if (tab.factories.length > 50) {
console.warn(`User ${username} tried to save a tab with too many factories!`);
tab.factories = tab.factories.slice(0, 50);
}

if (factory.notes && factory.notes.length > 1000) {
console.warn(`User ${username} tried to save a notes field that was too long!`);
factory.notes = factory.notes.substring(0, 1000);
}
tab.factories.forEach((factory: Factory) => {
if (factory.name.length > 200) {
console.warn(`User ${username} tried to save a factory name that was too long!`);
factory.name = factory.name.substring(0, 200);
}

if (factory.tasks) {
// Make sure it doesn't exceed a certain character limit
factory.tasks.forEach((task) => {
if (task.title.length > 200) {
console.warn(`User ${username} tried to save a factory task that was way too long!`);
task.title = task.title.substring(0, 200);
}
});
if (factory.notes && factory.notes.length > 1000) {
console.warn(`User ${username} tried to save a notes field that was too long!`);
factory.notes = factory.notes.substring(0, 1000);
}

// Make sure they can't take the piss with a stupid number of tasks
if (factory.tasks.length > 50) {
console.warn(`User ${username} tried to save a factory with too many tasks!`);
factory.tasks = factory.tasks.slice(0, 50);
if (factory.tasks) {
// Make sure it doesn't exceed a certain character limit
factory.tasks.forEach((task) => {
if (task.title.length > 200) {
console.warn(`User ${username} tried to save a factory task that was way too long!`);
task.title = task.title.substring(0, 200);
}
});

// Make sure they can't take the piss with a stupid number of tasks
if (factory.tasks.length > 50) {
console.warn(`User ${username} tried to save a factory with too many tasks!`);
factory.tasks = factory.tasks.slice(0, 50);
}
}
}
})
})

await FactoryData.findOneAndUpdate(
await PlannerStateSchema.findOneAndUpdate(
{ user: username },
{ data: factoryData, lastSaved: new Date() },
{ data: plannerState, lastSaved: new Date() },
{ new: true, upsert: true }
);

console.log(`Data saved for ${username}`);
// Delete any data that is in the old FactoryData collection. This will prevent /needsStateMigration being true
await FactoryData.deleteOne({ user: username });

console.log(`Planner State saved for ${username}`);

res.json({ message: 'Data saved successfully', userData: factoryData });
res.json({ message: 'Planner State saved successfully', userData: plannerState });
} catch (error) {
console.error(`Data save failed: ${error}`);
res.status(500).json({ message: 'Data save failed', error });
Expand All @@ -245,11 +265,11 @@
});

// Load Data Endpoint
app.get('/load', authenticate, async (req: AuthenticatedRequest & TypedRequestBody<{ data: any }>, res: Express.Response) => {

Check warning on line 268 in backend/backend.ts

View workflow job for this annotation

GitHub Actions / Build Backend

Unexpected any. Specify a different type
try {
const { username } = req.user as jwt.JwtPayload & { username: string };

const data = await FactoryData.findOne(
const data = await PlannerStateSchema.findOne(
{ user: username },
);

Expand All @@ -260,7 +280,7 @@
});

// Share link create endpoint
app.post('/share', optionalAuthenticate, shareRateLimit, async (req: AuthenticatedRequest & TypedRequestBody<{ data: any }>, res: Express.Response) => {

Check warning on line 283 in backend/backend.ts

View workflow job for this annotation

GitHub Actions / Build Backend

Unexpected any. Specify a different type
try {
const { username } = req.user as jwt.JwtPayload & { username: string };
const factoryData = req.body;
Expand Down
8 changes: 8 additions & 0 deletions backend/interfaces/FactoryInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,11 @@ export interface FactoryTab {
name: string;
factories: Factory[];
}

export interface PlannerState {
user: string;
currentTabId: string;
lastSaved: Date;
userOptions: string[];
tabs: FactoryTab[];
}
13 changes: 13 additions & 0 deletions backend/models/PlannerState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import mongoose from "mongoose";

const PlannerStateSchema = new mongoose.Schema(
{
user: { type: String, required: true },
currentTabId: { type: String, required: true },
tabs: { type: mongoose.Schema.Types.Mixed, required: true },
userOptions: [{ type: String, required: true }],
lastSaved: { type: Date, default: Date.now },
},
{ minimize: false}
);
export const PlannerState = mongoose.model('PlannerState', PlannerStateSchema);
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@
"unplugin-vue-components": "^0.27.2",
"unplugin-vue-router": "^0.10.0",
"vite": "^5.3.3",
"vite-plugin-vue-devtools": "^7.6.8",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "^2.0.3",
"vite-plugin-vue-devtools": "^7.6.8",
"vitest": "^2.1.5",
"vue-router": "^4.4.0",
"vue-tsc": "^2.0.26"
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/Loading.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
// Handle issues with Edge browser breaking reactivity.
setTimeout(() => {
showEdgeWarning.value = true
}, 5000)
}, 30000)
}

onMounted(() => {
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/ShareButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import eventBus from '@/utils/eventBus'

// Get user auth stuff from the app store
const { currentFactoryTab } = useAppStore()
const { currentTab, getFactories } = useAppStore()
const authStore = useAuthStore()

const apiUrl = config.apiUrl
Expand All @@ -39,13 +39,14 @@
const showCopyDialog = ref(false)

const createShareLink = async () => {
if (!currentFactoryTab.factories || currentFactoryTab.factories.length === 0) {
const factories = getFactories()
if (!factories || factories.length === 0) {
alert('No factory data to share!')
return
}

creating.value = true
link.value = await handleCreation(currentFactoryTab) ?? ''
link.value = await handleCreation(currentTab) ?? ''
creating.value = false

// If no link was returned assume server errors
Expand Down
47 changes: 31 additions & 16 deletions web/src/components/TabNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@
<div class="border-t-md d-flex bg-grey-darken-3 align-center justify-space-between w-100">
<div class="d-flex align-center" style="min-width: 0">
<v-tabs
v-model="appStore.currentFactoryTabIndex"
v-model="selectedTab"
color="deep-orange"
>
<v-tab
v-for="(item, index) in appStore.factoryTabs"
:key="item.id"
v-for="(tab) in appStore.getTabs()"
:key="tab.id"
class="text-none"
:ripple="!isCurrentTab(index)"
:slim="isCurrentTab(index)"
:value="index"
:ripple="!isCurrentTab(tab)"
:slim="isCurrentTab(tab)"
:value="tab.id"
>
<input
v-if="isCurrentTab(index) && isEditingName"
v-if="isCurrentTab(tab) && isEditingName"
v-model="currentTabName"
class="pa-1 rounded border bg-grey-darken-2"
@keyup.enter="onClickEditTabName"
>
<span v-else>
{{ item.name }}
{{ tab.name }}
</span>
<v-btn
v-if="isCurrentTab(index)"
v-if="isCurrentTab(tab)"
:key="`${isEditingName}`"
color="grey-darken-3"
:icon="`fas ${isEditingName ? 'fa-check': 'fa-pen'}`"
Expand All @@ -39,14 +39,14 @@
icon="fas fa-plus"
size="x-small"
variant="flat"
@click="appStore.addTab()"
@click="appStore.createNewTab()"
/>
</div>

<div class="d-flex align-center h-100 ga-2 mr-1">
<ShareButton />
<v-btn
v-if="appStore.factoryTabs.length > 1"
v-if="getTabsCount(appStore.getState())"
color="red rounded"
icon="fas fa-trash"
size="small"
Expand All @@ -61,24 +61,39 @@
<script setup lang="ts">
import { useAppStore } from '@/stores/app-store'
import { confirmDialog } from '@/utils/helpers'
import { FactoryTab } from '@/interfaces/planner/FactoryInterface'
import eventBus from '@/utils/eventBus'
import { getTabsCount } from '@/utils/plannerStateManagement'

const appStore = useAppStore()

// Take a shallow copy of the current tab ID
const selectedTab = ref(JSON.parse(JSON.stringify(appStore.currentTabId)))

const isEditingName = ref(false)
const currentTabName = ref(appStore.currentFactoryTab.name)
const currentTabName = ref(appStore.currentTab.name)

const isCurrentTab = (index:number) => index === appStore.currentFactoryTabIndex
const isCurrentTab = (tab: FactoryTab) => tab.id === appStore.currentTabId

const onClickEditTabName = () => {
isEditingName.value = !isEditingName.value
if (!isEditingName.value) {
appStore.currentFactoryTab.name = currentTabName.value
appStore.currentTab.name = currentTabName.value
}
}

watch(() => appStore.currentFactoryTabIndex, () => {
watch(() => appStore.currentTabId, () => {
isEditingName.value = false
currentTabName.value = appStore.currentFactoryTab.name
currentTabName.value = appStore.currentTab.name
})

watch(() => selectedTab.value, () => {
console.log('TabNavigation: selectedTab: Changing selected tab:', selectedTab.value)
appStore.changeCurrentTab(selectedTab.value)
})

eventBus.on('switchTab', (tabId: string) => {
selectedTab.value = tabId
})

const confirmDelete = () => {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/Templates.vue
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@

// This is a workaround for the templating bug where the data was passed as a reference, and would refuse to load the same template until the page is refreshed.
const data = JSON.parse(template.data) as Factory[]
prepareLoader(data)
prepareLoader(data, true)
dialog.value = false
}
</script>
47 changes: 22 additions & 25 deletions web/src/components/planner/Introduction.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,36 +60,18 @@
</v-dialog>
</template>
<script setup lang="ts">
import { defineEmits } from 'vue'

const props = defineProps<{
introShow: boolean
}>()
import { defineEmits, ref } from 'vue'
import eventBus from '@/utils/eventBus'

const showDialog = ref<boolean>(false)

onMounted(() => {
console.log('Intro show:', props.introShow)
showDialog.value = props.introShow
})

// Set up a watcher to close the dialog when the prop changes
watch(() => props.introShow, value => {
showDialog.value = value
})

// Set up a watcher if the dialogue is changed to closed, we emit the event by calling close()
watch(() => showDialog.value, value => {
if (!value) {
console.log('Closing intro via watch')
close()
}
})
// Grab from local storage if the user has already dismissed this popup
// If they have, don't show it again.
const introShow = ref<boolean>(!localStorage.getItem('dismissed-introduction'))

// eslint-disable-next-line func-call-spacing
const emit = defineEmits<{
(event: 'showDemo'): void;
(event: 'closeIntro'): void;
}>()

const showDemo = () => {
Expand All @@ -98,10 +80,25 @@
close()
}

const open = () => {
console.log('Introduction: Opening introduction')
showDialog.value = true
localStorage.setItem('dismissed-introduction', 'false')
}

const close = () => {
console.log('Closing introduction')
emit('closeIntro')
console.log('Introduction: Closing introduction')
showDialog.value = false
localStorage.setItem('dismissed-introduction', 'true')
}

eventBus.on('introShow', (show: boolean) => {
console.log('Introduction: Got introShow event', show)
show ? open() : close()
})

if (introShow.value) {
open()
}
</script>
<style lang="scss" scoped>
Expand Down
Loading
Loading