Added category creation interface
This commit is contained in:
@@ -11,15 +11,7 @@ const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.loc
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<header class="grid grid-cols-12 gap-4 px-2 py-4 border-b-[1px] border-b-foreground/10 font-bold">
|
<CalendarCategoryTableHeader />
|
||||||
<div class="col-span-6">
|
|
||||||
Nom
|
|
||||||
</div>
|
|
||||||
<div class="col-span-4">
|
|
||||||
Couleur
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2" />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<ScrollAreaRoot class="h-36 grow overflow-hidden ">
|
<ScrollAreaRoot class="h-36 grow overflow-hidden ">
|
||||||
<ScrollAreaViewport class="w-full h-full pr-4" as-child>
|
<ScrollAreaViewport class="w-full h-full pr-4" as-child>
|
||||||
@@ -40,5 +32,7 @@ const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.loc
|
|||||||
/>
|
/>
|
||||||
</ScrollAreaScrollbar>
|
</ScrollAreaScrollbar>
|
||||||
</ScrollAreaRoot>
|
</ScrollAreaRoot>
|
||||||
|
|
||||||
|
<CalendarCategoryTableFooter />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
130
components/calendar/category/TableFooter.vue
Normal file
130
components/calendar/category/TableFooter.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { PhPlus } from "@phosphor-icons/vue"
|
||||||
|
import { useToast } from "~/components/ui/toast"
|
||||||
|
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const { toast } = useToast()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
type FooterMode = "add" | "view"
|
||||||
|
const currentMode = ref<FooterMode>("view")
|
||||||
|
|
||||||
|
const rowRef = ref<HTMLDivElement | null>(null)
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const { focused: inputFocused } = useFocus(inputRef)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle view mode options
|
||||||
|
*/
|
||||||
|
type ToggleViewOptions = {
|
||||||
|
execution?: "now" | "nextTick"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle view mode
|
||||||
|
* @param options.execution - When to execute the toggle. "now" or "nextTick"
|
||||||
|
*/
|
||||||
|
function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
||||||
|
currentMode.value = "view"
|
||||||
|
|
||||||
|
if (options.execution === "now") {
|
||||||
|
inputFocused.value = false
|
||||||
|
} else {
|
||||||
|
nextTick(() => {
|
||||||
|
inputFocused.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle add mode
|
||||||
|
*/
|
||||||
|
function toggleAdd() {
|
||||||
|
currentMode.value = "add"
|
||||||
|
categorySkeleton.value = { name: "", color: "black" }
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
inputFocused.value = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickOutside(rowRef, () => toggleView({ execution: "nextTick" }))
|
||||||
|
onKeyStroke("Escape", () => toggleView({ execution: "now" }))
|
||||||
|
|
||||||
|
const { addCategoryFromSkeleton } = useCategoryStore()
|
||||||
|
const { categorySkeleton } = storeToRefs(useCategoryStore())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit the update
|
||||||
|
*/
|
||||||
|
async function submitNew() {
|
||||||
|
if (!categorySkeleton.value) return
|
||||||
|
const newCategoryName = toRaw(categorySkeleton.value).name
|
||||||
|
|
||||||
|
const { error } = await tryCatch(addCategoryFromSkeleton())
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast({
|
||||||
|
title: t("entity.category.addedToast.titleError", { category: newCategoryName }),
|
||||||
|
variant: "destructive",
|
||||||
|
duration: ToastLifetime.LONG
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleView({ execution: "now" })
|
||||||
|
toast({
|
||||||
|
title: t("entity.category.addedToast.title", { category: newCategoryName }),
|
||||||
|
variant: "success",
|
||||||
|
duration: ToastLifetime.SHORT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border-y-[1px] border-b-foreground/10 mr-4">
|
||||||
|
<form
|
||||||
|
ref="rowRef"
|
||||||
|
class="grid grid-cols-12 items-center gap-4 p-1 bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900"
|
||||||
|
@submit.prevent="submitNew"
|
||||||
|
>
|
||||||
|
<div v-if="currentMode === 'add'" class="col-span-1" />
|
||||||
|
<div
|
||||||
|
:class="cn({
|
||||||
|
'col-span-6': currentMode === 'view',
|
||||||
|
'col-span-5': currentMode === 'add'
|
||||||
|
})"
|
||||||
|
>
|
||||||
|
<template v-if="currentMode === 'view'">
|
||||||
|
<button
|
||||||
|
class="p-2 h-full w-full text-left underline-offset-4 hover:underline cursor-pointer"
|
||||||
|
@click="toggleAdd"
|
||||||
|
>
|
||||||
|
<PhPlus size="18" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="categorySkeleton.name"
|
||||||
|
type="text"
|
||||||
|
class="p-1 h-full w-full bg-transparent focus-visible:outline-none italic"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-4">
|
||||||
|
<template v-if="currentMode === 'add' && categorySkeleton">
|
||||||
|
<div class="-mx-2">
|
||||||
|
<InputColor
|
||||||
|
id="category-color"
|
||||||
|
v-model="categorySkeleton.color"
|
||||||
|
position="item-aligned"
|
||||||
|
theme="subtle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
19
components/calendar/category/TableHeader.vue
Normal file
19
components/calendar/category/TableHeader.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { PhListBullets } from "@phosphor-icons/vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="grid grid-cols-12 gap-4 px-2 py-4 border-b-[1px] border-b-foreground/10 font-bold mr-4">
|
||||||
|
<div class="col-span-1 flex items-end">
|
||||||
|
<PhListBullets size="20" />
|
||||||
|
</div>
|
||||||
|
<div class="col-span-5">
|
||||||
|
Nom
|
||||||
|
</div>
|
||||||
|
<div class="col-span-4">
|
||||||
|
Couleur
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2" />
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
@@ -34,10 +34,10 @@ function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
|||||||
currentMode.value = "view"
|
currentMode.value = "view"
|
||||||
|
|
||||||
if (options.execution === "now") {
|
if (options.execution === "now") {
|
||||||
inputFocused.value = false;
|
inputFocused.value = false
|
||||||
} else {
|
} else {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
inputFocused.value = false;
|
inputFocused.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,11 +46,11 @@ function toggleView(options: ToggleViewOptions = { execution: "now" }) {
|
|||||||
* Toggle edit mode
|
* Toggle edit mode
|
||||||
*/
|
*/
|
||||||
function toggleEdit() {
|
function toggleEdit() {
|
||||||
currentMode.value = "edit";
|
currentMode.value = "edit"
|
||||||
categorySkeleton.value = structuredClone(toRaw(category))
|
categorySkeleton.value = structuredClone(toRaw(category))
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
inputFocused.value = true;
|
inputFocused.value = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,6 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit the update
|
* Submit the update
|
||||||
* TODO: Implement the update logic
|
|
||||||
*/
|
*/
|
||||||
async function submitUpdate() {
|
async function submitUpdate() {
|
||||||
const { error } = await tryCatch(updateCategoryFromSkeleton())
|
const { error } = await tryCatch(updateCategoryFromSkeleton())
|
||||||
@@ -90,18 +89,20 @@ async function submitUpdate() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div ref="rowRef" class="relative">
|
||||||
<form
|
<form
|
||||||
ref="rowRef"
|
class="grid grid-cols-12 items-center gap-4 p-1 border-b-[1px] border-b-foreground/10"
|
||||||
class="grid grid-cols-12 items-center gap-4 p-1"
|
|
||||||
:class="cn(
|
:class="cn(
|
||||||
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' },
|
{ 'bg-slate-100 hover:bg-slate-200 dark:bg-slate-900 dark:hover:bg-slate-800': currentMode === 'edit' },
|
||||||
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' }
|
{ 'bg-transparent hover:bg-slate-50 dark:bg-transparent dark:hover:bg-slate-900': currentMode !== 'edit' }
|
||||||
)"
|
)"
|
||||||
@submit.prevent="submitUpdate"
|
@submit.prevent="submitUpdate"
|
||||||
>
|
>
|
||||||
|
<div class="col-span-1 pointer-events-none">
|
||||||
|
<span class="opacity-50 p-2">{{ category.id }}</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="col-span-6"
|
class="col-span-5"
|
||||||
>
|
>
|
||||||
<template v-if="currentMode === 'view'">
|
<template v-if="currentMode === 'view'">
|
||||||
<button
|
<button
|
||||||
@@ -134,7 +135,7 @@ async function submitUpdate() {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
<template v-else-if="currentMode === 'edit' && categorySkeleton">
|
||||||
<div class="-mx-2">
|
<div class="-mx-2">
|
||||||
<InputColor
|
<InputColor
|
||||||
id="category-color"
|
id="category-color"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const model = defineModel<RPGColor>({ default: "white" });
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UiSelect v-model="model">
|
<UiSelect v-model="model">
|
||||||
<UiSelectTrigger :id :class="cn({ 'h-auto': theme === 'subtle' })">
|
<UiSelectTrigger :id :class="cn({ 'h-9': theme === 'subtle' })">
|
||||||
<UiSelectValue
|
<UiSelectValue
|
||||||
:placeholder="$t('ui.colors.selectOne')"
|
:placeholder="$t('ui.colors.selectOne')"
|
||||||
class="bgc"
|
class="bgc"
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ export default defineI18nConfig(() => ({
|
|||||||
title: "Manage categories",
|
title: "Manage categories",
|
||||||
subtitle: "Add and change the categories of your calendar",
|
subtitle: "Add and change the categories of your calendar",
|
||||||
},
|
},
|
||||||
|
addedToast: {
|
||||||
|
title: "The category \"{category}\" has been added to the calendar.",
|
||||||
|
titleError: "An error has occured and the category \"{category}\" wasn't added to the calendar.",
|
||||||
|
},
|
||||||
updatedToast: {
|
updatedToast: {
|
||||||
title: "The category \"{category}\" has been successfuly updated.",
|
title: "The category \"{category}\" has been successfuly updated.",
|
||||||
titleError: "An error has occured and the category \"{category}\" wasn't updated.",
|
titleError: "An error has occured and the category \"{category}\" wasn't updated.",
|
||||||
@@ -409,6 +413,10 @@ export default defineI18nConfig(() => ({
|
|||||||
title: "Gestion des catégories",
|
title: "Gestion des catégories",
|
||||||
subtitle: "Créer et modifier les catégories de votre calendrier",
|
subtitle: "Créer et modifier les catégories de votre calendrier",
|
||||||
},
|
},
|
||||||
|
addedToast: {
|
||||||
|
title: "La catégorie \"{category}\" a été ajoutée au calendrier.",
|
||||||
|
titleError: "Une erreur s'est produite et la catégorie \"{category}\" n'a pas pu être ajoutée.",
|
||||||
|
},
|
||||||
updatedToast: {
|
updatedToast: {
|
||||||
title: "La catégorie \"{category}\" a été modifiée avec succès.",
|
title: "La catégorie \"{category}\" a été modifiée avec succès.",
|
||||||
titleError: "Une erreur s'est produite et la catégorie \"{category}\" n'a pas pu être modifiée.",
|
titleError: "Une erreur s'est produite et la catégorie \"{category}\" n'a pas pu être modifiée.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { categorySchema, type Category } from "./Category"
|
import type { Category } from "./Category"
|
||||||
import { dateSchema, type RPGDate } from "./Date"
|
import { dateSchema, type RPGDate } from "./Date"
|
||||||
|
|
||||||
export interface CalendarEvent {
|
export interface CalendarEvent {
|
||||||
@@ -26,7 +26,11 @@ export const postEventBodySchema = z.object({
|
|||||||
startDate: dateSchema.required(),
|
startDate: dateSchema.required(),
|
||||||
endDate: dateSchema.optional().nullable(),
|
endDate: dateSchema.optional().nullable(),
|
||||||
hidden: z.boolean().optional().nullable(),
|
hidden: z.boolean().optional().nullable(),
|
||||||
category: categorySchema.optional().nullable(),
|
category: z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
name: z.string(),
|
||||||
|
color: z.string().default("black")
|
||||||
|
}).optional().nullable(),
|
||||||
}),
|
}),
|
||||||
calendarId: z.number({ coerce: true }).int().positive()
|
calendarId: z.number({ coerce: true }).int().positive()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ import { z } from "zod"
|
|||||||
import type { RPGColor } from "./Color"
|
import type { RPGColor } from "./Color"
|
||||||
|
|
||||||
export interface Category {
|
export interface Category {
|
||||||
id: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
color?: RPGColor
|
color?: RPGColor
|
||||||
}
|
}
|
||||||
|
|
||||||
export const categorySchema = z.object({
|
export const categorySchema = z.object({
|
||||||
id: z.number().int(),
|
category: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
color: z.string().default("black"),
|
color: z.string().default("black"),
|
||||||
|
}),
|
||||||
|
calendarId: z.number({ coerce: true }).int().positive()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
.from("calendar_event_categories")
|
.from("calendar_event_categories")
|
||||||
.update(
|
.update(
|
||||||
{
|
{
|
||||||
name: bodyData.name,
|
name: bodyData.category.name,
|
||||||
color: bodyData.color,
|
color: bodyData.category.color,
|
||||||
|
calendar_id: bodyData.calendarId
|
||||||
} as never
|
} as never
|
||||||
)
|
)
|
||||||
.eq("id", params.id)
|
.eq("id", params.id)
|
||||||
|
|||||||
56
server/api/calendars/categories/create.post.ts
Normal file
56
server/api/calendars/categories/create.post.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { serverSupabaseClient } from "#supabase/server";
|
||||||
|
import { type Category, categorySchema } from "@/models/Category";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const client = await serverSupabaseClient(event)
|
||||||
|
|
||||||
|
const { data: bodyData, error: bodyError } = await readValidatedBody(event, body => categorySchema.safeParse(body))
|
||||||
|
|
||||||
|
if (bodyError) {
|
||||||
|
const error = createError({
|
||||||
|
cause: "Utilisateur",
|
||||||
|
fatal: false,
|
||||||
|
statusCode: 401,
|
||||||
|
statusMessage: "Validation Error",
|
||||||
|
message: "Erreur de validation du schéma, probablement dûe à une erreur utilisateur.",
|
||||||
|
data: {
|
||||||
|
errors: bodyError.issues.map(issue => ({
|
||||||
|
path: issue.path,
|
||||||
|
message: issue.message,
|
||||||
|
code: issue.code
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error } = await client
|
||||||
|
.from("calendar_event_categories")
|
||||||
|
.insert(
|
||||||
|
{
|
||||||
|
name: bodyData.category.name,
|
||||||
|
color: bodyData.category.color,
|
||||||
|
calendar_id: bodyData.calendarId
|
||||||
|
} as never
|
||||||
|
)
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
color
|
||||||
|
`)
|
||||||
|
.single<Category>()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err) {
|
||||||
|
throw createError({
|
||||||
|
cause: "Serveur",
|
||||||
|
status: 500,
|
||||||
|
fatal: false,
|
||||||
|
message: "Une erreur inconnue est survenue."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Category } from "~/models/Category";
|
import type { Category } from "~/models/Category";
|
||||||
|
|
||||||
export const useCategoryStore = defineStore("calendar-category", () => {
|
export const useCategoryStore = defineStore("calendar-category", () => {
|
||||||
const { categories } = storeToRefs(useCalendar())
|
const { activeCalendar, categories } = storeToRefs(useCalendar())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dummy event to hold creation data
|
* Dummy event to hold creation data
|
||||||
@@ -21,16 +21,38 @@ export const useCategoryStore = defineStore("calendar-category", () => {
|
|||||||
categorySkeleton.value = null
|
categorySkeleton.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addCategoryFromSkeleton() {
|
||||||
|
if (!categorySkeleton.value) return
|
||||||
|
|
||||||
|
abortController = new AbortController()
|
||||||
|
|
||||||
|
isCreatingCategory.value = true
|
||||||
|
|
||||||
|
const { data, error } = await tryCatch(
|
||||||
|
$fetch<Category>("/api/calendars/categories/create", { method: "POST", body: { category: categorySkeleton.value, calendarId: activeCalendar.value?.id }, signal: abortController.signal })
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
isCreatingCategory.value = false
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the category in the store
|
||||||
|
categories.value.push(data)
|
||||||
|
|
||||||
|
abortController = null
|
||||||
|
isCreatingCategory.value = false
|
||||||
|
resetSkeleton()
|
||||||
|
}
|
||||||
|
|
||||||
async function updateCategoryFromSkeleton() {
|
async function updateCategoryFromSkeleton() {
|
||||||
if (!categorySkeleton.value) return
|
if (!categorySkeleton.value) return
|
||||||
|
|
||||||
abortController = new AbortController()
|
abortController = new AbortController()
|
||||||
isUpdatingCategory.value = true
|
isUpdatingCategory.value = true
|
||||||
|
|
||||||
const body = categorySkeleton.value
|
|
||||||
|
|
||||||
const { data, error } = await tryCatch(
|
const { data, error } = await tryCatch(
|
||||||
$fetch<Category>(`/api/calendars/categories/${categorySkeleton.value.id}`, { method: "PATCH", body, signal: abortController.signal })
|
$fetch<Category>(`/api/calendars/categories/${categorySkeleton.value.id}`, { method: "PATCH", body: { category: categorySkeleton.value, calendarId: activeCalendar.value?.id }, signal: abortController.signal })
|
||||||
)
|
)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -48,6 +70,7 @@ export const useCategoryStore = defineStore("calendar-category", () => {
|
|||||||
|
|
||||||
abortController = null
|
abortController = null
|
||||||
isUpdatingCategory.value = false
|
isUpdatingCategory.value = false
|
||||||
|
resetSkeleton()
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -57,6 +80,7 @@ export const useCategoryStore = defineStore("calendar-category", () => {
|
|||||||
operationInProgress,
|
operationInProgress,
|
||||||
categorySkeleton,
|
categorySkeleton,
|
||||||
resetSkeleton,
|
resetSkeleton,
|
||||||
|
addCategoryFromSkeleton,
|
||||||
updateCategoryFromSkeleton
|
updateCategoryFromSkeleton
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -440,6 +440,20 @@ create policy "Allow anonymous access to published event categories" on public.c
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
create policy "Allow GMs to add new events categories"
|
||||||
|
on public.calendar_event_categories
|
||||||
|
for insert
|
||||||
|
with check (
|
||||||
|
exists (
|
||||||
|
select 1
|
||||||
|
from public.calendars c
|
||||||
|
join public.worlds w on w.id = c.world_id
|
||||||
|
where
|
||||||
|
c.id = calendar_event_categories.calendar_id
|
||||||
|
and w.gm_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
create policy "Allow GMs to update their events categories"
|
create policy "Allow GMs to update their events categories"
|
||||||
on public.calendar_event_categories
|
on public.calendar_event_categories
|
||||||
for update
|
for update
|
||||||
|
|||||||
Reference in New Issue
Block a user