@@ -101,7 +91,7 @@
class="q-mb-md"
>
- Settings
+ Settings
+
+
+ Public system content is visible to everyone, so we turned on Public access. Switch it
+ off if this block should stay restricted.
+
@@ -164,7 +165,7 @@
class="q-mb-md"
>
- Permissions
+ Permissions
@@ -174,7 +175,7 @@
bordered
>
- Attached Files
+ Attached Files
{{ file.friendlyName }}
+ (opens in new window)
+
+ {{ formError }}
+
+
+ >
+
+
+ {{ isNew ? "Create" : "Save" }}
+
+
import { computed, inject, onMounted, ref } from "vue"
-import { useRoute, useRouter } from "vue-router"
+import { useRoute, useRouter, onBeforeRouteLeave } from "vue-router"
import { useQuasar } from "quasar"
import { useFetch } from "@/composables/ViperFetch"
+import { useUnsavedChanges } from "@/composables/use-unsaved-changes"
+import BreadcrumbHeading from "@/components/BreadcrumbHeading.vue"
import PermissionSelector from "@/CMS/components/PermissionSelector.vue"
+import StatusBanner from "@/components/StatusBanner.vue"
import type { CmsContentBlock, CmsContentBlockFile, CmsContentHistoryItem, CmsFile } from "@/CMS/types/"
const apiURL = inject("apiURL") + "CMS/content"
@@ -263,6 +283,7 @@ const isNew = computed(() => blockId.value === null)
const formRef = ref()
const saving = ref(false)
+const formError = ref("")
const emptyBlock = (): CmsContentBlock => ({
contentBlockId: 0,
@@ -283,10 +304,17 @@ const emptyBlock = (): CmsContentBlock => ({
})
const block = ref(emptyBlock())
+
+const { setInitialState, resetDirtyState, confirmClose } = useUnsavedChanges(block)
+
+// Prompt before leaving with unsaved edits, matching the Effort forms' guard.
+onBeforeRouteLeave(async () => await confirmClose())
+
const sectionPaths = ref([])
const history = ref([])
const selectedHistory = ref(null)
const viewingVersion = ref(false)
+const autoEnabledPublicAccess = ref(false)
const fileToAttach = ref(null)
const fileOptions = ref([])
@@ -310,6 +338,8 @@ async function loadBlock() {
return
}
block.value = res.result
+ autoEnabledPublicAccess.value = false
+ setInitialState()
await loadHistory()
}
@@ -376,9 +406,29 @@ function detachFile(file: CmsContentBlockFile) {
block.value.files = block.value.files.filter((f) => f.fileGuid !== file.fileGuid)
}
+// "Public" system content is publicly visible by convention, but AllowPublicAccess is the only
+// field that actually gates unauthenticated access. Enable it for the user and tell them, so the
+// two stay in sync without silently exposing a block they meant to keep restricted.
+function onSystemChange(value: string | null) {
+ if (value === "Public" && !block.value.allowPublicAccess) {
+ block.value.allowPublicAccess = true
+ autoEnabledPublicAccess.value = true
+ } else {
+ autoEnabledPublicAccess.value = false
+ }
+}
+
+// Fires when the form's submit-time validation fails. The q-form focuses the first invalid
+// field; this surfaces a matching message next to the Create button so the failure is obvious
+// without scrolling back up to the field.
+function onValidationError() {
+ formError.value = isNew.value
+ ? "Please complete the required fields before creating this content block."
+ : "Please complete the required fields before saving your changes."
+}
+
async function saveBlock() {
- const valid = await formRef.value?.validate()
- if (!valid) return
+ formError.value = ""
saving.value = true
const payload = {
@@ -420,20 +470,23 @@ async function saveBlock() {
}
if (!res.success) {
- $q.notify({ type: "negative", message: res.errors?.[0] ?? "Failed to save content block" })
+ formError.value = res.errors?.[0] ?? "Failed to save content block"
return
}
$q.notify({ type: "positive", message: isNew.value ? "Content block created" : "Content block saved" })
viewingVersion.value = false
selectedHistory.value = null
+ autoEnabledPublicAccess.value = false
if (isNew.value) {
void router.push({ name: "CmsContentBlockEdit", params: { id: res.result.contentBlockId } })
block.value = res.result
+ resetDirtyState()
await loadHistory()
} else {
block.value = res.result
+ resetDirtyState()
await loadHistory()
}
}
@@ -449,6 +502,11 @@ async function restoreBlock() {
onMounted(() => {
loadSectionPaths()
loadBlock()
+ // loadBlock sets the baseline for an existing block after it loads; a brand-new form's
+ // baseline is just the empty block, so capture it synchronously here.
+ if (isNew.value) {
+ setInitialState()
+ }
})
diff --git a/VueApp/src/CMS/pages/ContentBlocks.vue b/VueApp/src/CMS/pages/ContentBlocks.vue
index d0b0f1d80..557055115 100644
--- a/VueApp/src/CMS/pages/ContentBlocks.vue
+++ b/VueApp/src/CMS/pages/ContentBlocks.vue
@@ -1,15 +1,14 @@
-
Content Blocks
+ Manage Content Blocks
@@ -32,9 +31,10 @@
v-model="filters.system"
dense
options-dense
- clearable
+ emit-value
+ map-options
label="System"
- :options="['Viper', 'Public']"
+ :options="systemOptions"
@update:model-value="loadBlocks"
/>
@@ -63,6 +63,14 @@
+
+
+