Skip to content

Commit 0256363

Browse files
Merge remote-tracking branch 'upstream/master' into christian/colors
2 parents 669afbc + 66db814 commit 0256363

34 files changed

+2479
-183
lines changed

assets/vue/components/page/Layout.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<SectionHeader :title="t('Pages')">
2+
<SectionHeader :title="computedTitle">
33
<BaseButton
44
v-if="menuItems.length"
55
icon="dots-vertical"
@@ -23,7 +23,7 @@
2323
<script setup>
2424
import BaseButton from "../basecomponents/BaseButton.vue"
2525
import BaseMenu from "../basecomponents/BaseMenu.vue"
26-
import { provide, ref, watch } from "vue"
26+
import { provide, ref, watch, computed } from "vue"
2727
import { useRoute } from "vue-router"
2828
import { useI18n } from "vue-i18n"
2929
import SectionHeader from "../layout/SectionHeader.vue"
@@ -42,8 +42,16 @@ watch(
4242
() => {
4343
menuItems.value = []
4444
},
45-
{ inmediate: true },
45+
{ immediate: true }
4646
)
4747
48+
const computedTitle = computed(() => {
49+
if (route.path.includes("/resources/pages/layouts")) {
50+
return ''
51+
}
52+
53+
return t("Pages")
54+
})
55+
4856
const toggleMenu = (event) => menu.value.toggle(event)
4957
</script>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<template>
2+
<div class="flex flex-col space-y-6 w-full">
3+
<div
4+
v-if="layoutColumns.length"
5+
class="flex gap-4 w-full"
6+
>
7+
<div
8+
v-for="column in layoutColumns"
9+
:key="column.id"
10+
class="flex-1 border border-gray-300 rounded p-4 bg-gray-50 min-h-[200px]"
11+
>
12+
<h3 class="text-center font-semibold mb-4">
13+
{{ t("Column") }} {{ column.id }}
14+
</h3>
15+
16+
<draggable
17+
v-model="column.blocks"
18+
group="blocks"
19+
animation="200"
20+
item-key="id"
21+
class="space-y-2"
22+
>
23+
<template #item="{ element }">
24+
<div
25+
class="p-2 bg-white border rounded shadow cursor-move flex justify-between items-center"
26+
>
27+
<span>{{ element.name }}</span>
28+
<BaseIcon
29+
icon="drag"
30+
v-if="!readonly"
31+
/>
32+
</div>
33+
</template>
34+
</draggable>
35+
</div>
36+
</div>
37+
<div
38+
v-if="!readonly"
39+
class="w-full bg-white p-4 rounded shadow"
40+
>
41+
<h2 class="text-lg font-semibold mb-2">
42+
{{ t("Blocks Palette") }}
43+
</h2>
44+
<div class="flex flex-wrap gap-3">
45+
<div
46+
v-for="block in blocksPalette"
47+
:key="block.id"
48+
class="cursor-pointer bg-gray-100 p-3 rounded shadow hover:bg-gray-200 flex items-center gap-2"
49+
@click="addBlockToFirstColumn(block)"
50+
>
51+
<BaseIcon
52+
:icon="block.icon"
53+
class="text-primary"
54+
/>
55+
<span>{{ block.name }}</span>
56+
</div>
57+
</div>
58+
</div>
59+
</div>
60+
</template>
61+
<script setup>
62+
import { ref, watch } from "vue"
63+
import { useI18n } from "vue-i18n"
64+
import draggable from "vuedraggable"
65+
import BaseIcon from "../../components/basecomponents/BaseIcon.vue"
66+
67+
const props = defineProps({
68+
modelValue: Object,
69+
templateOptions: Array,
70+
readonly: Boolean,
71+
})
72+
73+
const emit = defineEmits(["update:modelValue"])
74+
75+
const { t } = useI18n()
76+
77+
const layoutColumns = ref([])
78+
const pageId = ref(null)
79+
const pageTitle = ref(null)
80+
81+
const blocksPalette = [
82+
{ id: "text", name: "Text Block", icon: "text" },
83+
{ id: "image", name: "Image Block", icon: "image" },
84+
{ id: "button", name: "Button Block", icon: "button" },
85+
]
86+
87+
watch(
88+
() => props.modelValue,
89+
(newVal) => {
90+
if (newVal?.page?.layout?.columns?.length) {
91+
pageId.value = newVal.page.id ?? null
92+
pageTitle.value = newVal.page.title ?? null
93+
layoutColumns.value = JSON.parse(
94+
JSON.stringify(newVal.page.layout.columns)
95+
)
96+
} else {
97+
pageId.value = null
98+
pageTitle.value = null
99+
layoutColumns.value = []
100+
}
101+
emitLayout()
102+
},
103+
{ immediate: true }
104+
)
105+
106+
function addBlockToFirstColumn(block) {
107+
if (!layoutColumns.value.length) return
108+
layoutColumns.value[0].blocks.push({
109+
...block,
110+
id: Date.now(),
111+
})
112+
emitLayout()
113+
}
114+
115+
function emitLayout() {
116+
emit("update:modelValue", {
117+
page: {
118+
id: pageId.value ?? null,
119+
title: pageTitle.value ?? null,
120+
layout: {
121+
columns: layoutColumns.value,
122+
},
123+
},
124+
})
125+
}
126+
</script>
127+
128+
<style scoped>
129+
.min-h-\[200px\] {
130+
min-height: 200px;
131+
}
132+
</style>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<template>
2+
<div class="flex gap-6 flex-wrap w-full">
3+
<div
4+
v-for="col in layout?.page?.layout?.columns"
5+
:key="col.id"
6+
:style="{ width: col.width }"
7+
class="border border-gray-300 rounded p-4 min-h-[200px] bg-white shadow flex-1"
8+
>
9+
<h3 class="text-center font-semibold mb-4">Column {{ col.id }}</h3>
10+
11+
<div class="space-y-4">
12+
<component
13+
v-for="block in col.blocks"
14+
:is="getBlockComponent(block.type)"
15+
:key="block.id"
16+
:block="block"
17+
/>
18+
</div>
19+
</div>
20+
</div>
21+
</template>
22+
<script setup>
23+
import blockComponents from "./blocks"
24+
25+
const props = defineProps({
26+
layout: {
27+
type: Object,
28+
default: null,
29+
},
30+
})
31+
32+
function getBlockComponent(type) {
33+
if (!type) {
34+
return {
35+
template: `<div class="text-red-600">Block type missing</div>`,
36+
}
37+
}
38+
39+
return (
40+
blockComponents[type] || {
41+
template: `<div class="text-red-600">Unknown block type: ${type}</div>`,
42+
}
43+
)
44+
}
45+
</script>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<template>
2+
<div class="p-4">
3+
<button
4+
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
5+
@click="handleClick"
6+
>
7+
{{ block?.label || "Click me" }}
8+
</button>
9+
</div>
10+
</template>
11+
12+
<script setup>
13+
import { useRouter } from "vue-router"
14+
15+
const props = defineProps({
16+
block: Object,
17+
})
18+
19+
const router = useRouter()
20+
21+
function handleClick() {
22+
if (props.block?.url) {
23+
router.push(props.block.url)
24+
} else {
25+
alert("No URL specified.")
26+
}
27+
}
28+
</script>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<div class="p-4">
3+
<img
4+
:src="block?.src || 'https://via.placeholder.com/150'"
5+
:alt="block?.alt || 'Image Block'"
6+
class="max-w-full rounded shadow"
7+
/>
8+
</div>
9+
</template>
10+
11+
<script setup>
12+
defineProps({
13+
block: Object
14+
})
15+
</script>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<div class="text-gray-800">
3+
{{ block.content || "Empty Text Block" }}
4+
</div>
5+
</template>
6+
7+
<script setup>
8+
const props = defineProps({
9+
block: {
10+
type: Object,
11+
required: true,
12+
},
13+
})
14+
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import BlockText from "./BlockText.vue"
2+
import BlockImage from "./BlockImage.vue"
3+
import BlockButton from "./BlockButton.vue"
4+
5+
export default {
6+
text: BlockText,
7+
image: BlockImage,
8+
button: BlockButton,
9+
}

assets/vue/composables/auth/login.js

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function isValidHttpUrl(string) {
99
try {
1010
const url = new URL(string);
1111
return url.protocol === "http:" || url.protocol === "https:";
12-
} catch {
12+
} catch (_) {
1313
return false;
1414
}
1515
}
@@ -31,23 +31,31 @@ export function useLogin() {
3131
try {
3232
const responseData = await securityService.login(payload);
3333

34-
// Step 1: Handle 2FA
34+
// Check if the backend demands 2FA and no TOTP was provided yet
3535
if (responseData.requires2FA && !payload.totp) {
3636
requires2FA.value = true;
3737
return { success: false, requires2FA: true };
3838
}
3939

40-
// Step 2: Handle explicit error message
40+
// Check rotate password flow
41+
if (responseData.rotate_password && responseData.redirect) {
42+
window.location.href = responseData.redirect;
43+
return { success: true, rotate: true };
44+
}
45+
46+
// Handle explicit backend error message
4147
if (responseData.error) {
4248
showErrorNotification(responseData.error);
4349
return { success: false, error: responseData.error };
4450
}
4551

46-
// Step 3: Set user and load platform config
47-
securityStore.setUser(responseData);
48-
await platformConfigurationStore.initialize();
52+
// Special flow for terms acceptance
53+
if (responseData.load_terms && responseData.redirect) {
54+
window.location.href = responseData.redirect;
55+
return { success: true, redirect: responseData.redirect };
56+
}
4957

50-
// Step 4: Honor a redirect query parameter
58+
// Handle external redirect param
5159
const redirectParam = route.query.redirect?.toString();
5260
if (redirectParam) {
5361
if (isValidHttpUrl(redirectParam)) {
@@ -58,39 +66,43 @@ export function useLogin() {
5866
return { success: true };
5967
}
6068

61-
// Step 5: Handle "load terms" flow
62-
if (responseData.load_terms && responseData.redirect) {
69+
if (responseData.redirect) {
6370
window.location.href = responseData.redirect;
6471
return { success: true };
6572
}
6673

67-
// Step 6: Default post-login redirect based on roles
68-
const setting = platformConfigurationStore.getSetting(
69-
"registration.redirect_after_login"
70-
);
74+
securityStore.setUser(responseData);
75+
await platformConfigurationStore.initialize();
76+
77+
// Handle redirect param again after login
78+
if (route.query.redirect) {
79+
await router.replace({ path: route.query.redirect.toString() });
80+
return { success: true };
81+
}
82+
83+
// Determine post-login route from settings
84+
const setting = platformConfigurationStore.getSetting("registration.redirect_after_login");
7185
let target = "/";
7286

7387
if (setting && typeof setting === "string") {
7488
try {
7589
const map = JSON.parse(setting);
7690
const roles = responseData.roles || [];
77-
const profile = roles.includes("ROLE_ADMIN")
78-
? "ADMIN"
79-
: roles.includes("ROLE_SESSION_MANAGER")
80-
? "SESSIONADMIN"
81-
: roles.includes("ROLE_TEACHER")
82-
? "COURSEMANAGER"
83-
: roles.includes("ROLE_STUDENT_BOSS")
84-
? "STUDENT_BOSS"
85-
: roles.includes("ROLE_DRH")
86-
? "DRH"
87-
: roles.includes("ROLE_INVITEE")
88-
? "INVITEE"
89-
: roles.includes("ROLE_STUDENT")
90-
? "STUDENT"
91-
: null;
9291

92+
const getProfile = () => {
93+
if (roles.includes("ROLE_ADMIN")) return "ADMIN";
94+
if (roles.includes("ROLE_SESSION_MANAGER")) return "SESSIONADMIN";
95+
if (roles.includes("ROLE_TEACHER")) return "COURSEMANAGER";
96+
if (roles.includes("ROLE_STUDENT_BOSS")) return "STUDENT_BOSS";
97+
if (roles.includes("ROLE_DRH")) return "DRH";
98+
if (roles.includes("ROLE_INVITEE")) return "INVITEE";
99+
if (roles.includes("ROLE_STUDENT")) return "STUDENT";
100+
return null;
101+
};
102+
103+
const profile = getProfile();
93104
const value = profile && map[profile] ? map[profile] : "";
105+
94106
switch (value) {
95107
case "user_portal.php":
96108
case "index.php":

0 commit comments

Comments
 (0)