Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 34 additions & 2 deletions resources/js/common/layouts/AppLayout.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import SystemInfo from '@/common/components/SystemInfo.vue';
import {t} from '@craftcms/cp/utilities/translate.ts.mjs';
import {computed, reactive, ref, useTemplateRef, watch} from 'vue';
import {computed, reactive, ref, useSlots, useTemplateRef, watch} from 'vue';
import CpSidebar from '@/common/components/CpSidebar.vue';
import {useMediaQuery} from '@vueuse/core';
import {Head, type InertiaForm, usePage} from '@inertiajs/vue3';
Expand All @@ -18,6 +18,7 @@
import CalloutReadOnly from '@/common/components/CalloutReadOnly.vue';
import UserMenu from '@/common/components/UserMenu.vue';
import FlashMessages from '@/common/components/FlashMessages.vue';
import Pane from '@/common/components/Pane.vue';

interface SaveOptions {
redirect?: boolean;
Expand Down Expand Up @@ -56,6 +57,7 @@
}>();

const {errorFlash, successFlash} = useFlash();
const slots = useSlots();
const crumbs = computed(() => page.props.crumbs ?? null);
const formActionItems = computed(() => [
...props.defaultFormActions.map(defaultFormActionItem),
Expand All @@ -66,6 +68,7 @@
...(props.additionalSkipLinks ?? []),
]);
const readOnly = computed(() => page.props.readOnly);
const hasDetails = computed(() => Boolean(slots.details));
const sidebarToggle = useTemplateRef('sidebarToggle');
const {announcement, announce} = useAnnouncer();

Expand Down Expand Up @@ -279,7 +282,21 @@
<template v-if="readOnly">
<CalloutReadOnly />
</template>
<slot></slot>
<template v-if="hasDetails">
Comment thread
riasvdv marked this conversation as resolved.
<div class="content-with-details">
<div>
<slot></slot>
</div>
<aside>
<Pane appearance="raised">
<div class="details">
<slot name="details"></slot>
</div>
</Pane>
</aside>
</div>
</template>
<slot v-else></slot>
</div>
</component>
</main>
Expand Down Expand Up @@ -349,6 +366,21 @@
max-width: none;
}

.content-with-details {
display: grid;
gap: var(--c-spacing-md);

@container (width >= 768px) {
grid-template-columns: minmax(0, 1fr) clamp(12rem, 20%, 16rem);
align-items: start;
}
}

.details {
display: grid;
gap: var(--c-spacing-md);
}

@media screen and (min-width: 1024px) {
.cp {
grid-template-columns: v-bind(sidebarWidth) minmax(0, 1fr);
Expand Down
68 changes: 61 additions & 7 deletions resources/js/modules/permissions/components/PermissionList.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import {t} from '@craftcms/cp';
import {
getNestedKeys,
hasNested,
Expand All @@ -13,20 +14,52 @@
defineProps<{
modelValue: Array<string>;
permissions?: Record<string, PermissionItem>;
heading?: string;
permissionKeys?: Array<string>;
disabled?: boolean;
level?: number;
}>(),
{permissions: () => ({}), modelValue: () => [], disabled: false, level: 0}
{
permissions: () => ({}),
modelValue: () => [],
permissionKeys: () => [],
disabled: false,
level: 0,
}
);

function allSelected() {
if (!props.permissionKeys.length) {
return false;
}

const selected = new Set(props.modelValue);

return props.permissionKeys.every((key) => selected.has(key));
}

function toggleAll() {
if (allSelected()) {
const keysToRemove = new Set(props.permissionKeys);
emit(
'update:modelValue',
props.modelValue.filter((key) => !keysToRemove.has(key))
);
return;
}

emit('update:modelValue', [
...new Set([...props.modelValue, ...props.permissionKeys]),
]);
}

function toggleItem(key: string) {
const lowerKey = key.toLowerCase();
const index = props.modelValue.indexOf(lowerKey);
const index = props.modelValue.indexOf(key);
if (index === -1) {
emit('update:modelValue', [...props.modelValue, lowerKey]);
emit('update:modelValue', [...props.modelValue, key]);
} else {
const keysToRemove = new Set([
lowerKey,
key,
...getNestedKeys(props.permissions[key]),
]);
emit(
Expand All @@ -38,6 +71,27 @@
</script>

<template>
<div v-if="heading" class="flex gap-2 items-center">
<h3 class="mb-1 text-base">
{{ heading }}
</h3>

<craft-button
type="button"
size="small"
appearance="plain"
:disabled="disabled"
@click="toggleAll"
>
<template v-if="allSelected()">
{{ t('Deselect all') }}
</template>
<template v-else>
{{ t('Select all') }}
</template>
</craft-button>
</div>

<ul
class="group"
v-for="(item, key) in permissions"
Expand All @@ -49,7 +103,7 @@
<li>
<CraftCheckbox
:label="item.label"
:model-value="modelValue.includes(key.toLowerCase())"
:model-value="modelValue.includes(key)"
:value="key"
:disabled="disabled"
@update:model-value="toggleItem(key)"
Expand All @@ -75,7 +129,7 @@
v-if="hasNested(item)"
:permissions="item.nested"
:model-value="modelValue"
:disabled="disabled || !modelValue.includes(item.key.toLowerCase())"
:disabled="disabled || !modelValue.includes(item.key)"
@update:model-value="emit('update:modelValue', $event)"
:level="level! + 1"
/>
Expand Down
16 changes: 6 additions & 10 deletions resources/js/modules/permissions/helpers/permissions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
export interface PermissionItem {
key: string;
nested: Record<string, PermissionItem>;
label: string;
info?: string;
warning?: string;
}
export type PermissionItem = CraftCms.Cms.User.Data.Permission;

export function hasNested(item: PermissionItem) {
export function hasNested(
item: PermissionItem
): item is PermissionItem & {nested: Record<string, PermissionItem>} {
return (
item.nested &&
!!item.nested &&
typeof item.nested === 'object' &&
!Array.isArray(item.nested) &&
Object.keys(item.nested).length > 0
Expand All @@ -21,7 +17,7 @@ export function getNestedKeys(item: PermissionItem | undefined): Array<string> {
}

return Object.values(item.nested).flatMap((child: PermissionItem) => [
child.key.toLowerCase(),
child.key,
...getNestedKeys(child),
]);
}
15 changes: 8 additions & 7 deletions resources/js/modules/settings/composables/useSettingsSave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ export function useSettingsSave<T extends Record<string, any>>(
});

function save({redirect = true} = {}) {
let submitOptions = {};
if (redirect) {
submitOptions = {
preserveScroll: true,
preserveState: true,
};
}
const submitOptions = redirect
? {
preserveScroll: true,
preserveState: true,
}
: {
replace: true,
Comment thread
riasvdv marked this conversation as resolved.
};

form
.clearErrors()
Expand Down
124 changes: 124 additions & 0 deletions resources/js/pages/graphql/schemas/Edit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<script setup lang="ts">
import {t} from '@craftcms/cp';
import AppLayout from '@/common/layouts/AppLayout.vue';
import Pane from '@/common/components/Pane.vue';
import CraftInput from '@craftcms/cp/vue/CraftInput.vue';
import CraftSwitch from '@craftcms/cp/vue/CraftSwitch.vue';
import PermissionList from '@/modules/permissions/components/PermissionList.vue';
import {useSettingsSave} from '@/modules/settings/composables/useSettingsSave';
import {store, update} from '@actions/Gql/SchemasController';
import {useForm} from '@inertiajs/vue3';

type TokenData = Pick<
CraftCms.Cms.Gql.Data.GqlToken,
'id' | 'enabled' | 'expiryDate'
>;

interface SchemaForm {
name: string;
permissions: Array<string>;
enabled: boolean;
expiryDate: string;
}

type PermissionGroup = Omit<
CraftCms.Cms.User.Data.PermissionGroup,
'permissions'
> & {
permissions: Record<string, CraftCms.Cms.User.Data.Permission>;
};

const props = defineProps<{
schema: CraftCms.Cms.Gql.Data.GqlSchema;
token: TokenData | null;
permissions: Array<PermissionGroup>;
readOnly?: boolean;
}>();

const form = useForm<SchemaForm>({
name: props.schema.name ?? '',
permissions: props.schema.scope ?? [],
enabled: props.token?.enabled ?? true,
expiryDate: props.token?.expiryDate ?? '',
});

const routeAction = () => {
if (!props.schema.id) {
return store();
}

return update({
schemaId: props.schema.isPublic ? 'public' : props.schema.id,
});
};

const {save} = useSettingsSave(form, routeAction);
</script>

<template>
<AppLayout :form="form" @save="save">
<Pane appearance="raised">
<div class="grid gap-3">
<CraftInput
v-if="!schema.isPublic"
:label="t('Name')"
:help-text="
t('What this schema will be called in the control panel.')
"
id="name"
name="name"
v-model="form.name"
:error="form.errors.name"
:disabled="readOnly"
required
autofocus
/>

<hr v-if="!schema.isPublic" />

<section class="grid gap-3">
<h2 class="text-base">
{{
t('Choose the available content for querying with this schema:')
}}
</h2>

<div
v-for="group in permissions"
:key="group.handle"
class="user-permissions"
>
<PermissionList
:heading="group.heading"
:permissions="group.permissions"
:permission-keys="group.keys"
v-model="form.permissions"
:disabled="readOnly"
/>
</div>
</section>
</div>
</Pane>

<template v-if="schema.isPublic" #details>
<CraftSwitch
:label="t('Enabled')"
id="enabled"
name="enabled"
v-model="form.enabled"
:disabled="readOnly"
:error="form.errors.enabled"
/>

<CraftInput
:label="t('Expiry Date')"
id="expiryDate"
name="expiryDate"
type="datetime-local"
v-model="form.expiryDate"
:disabled="readOnly"
:error="form.errors.expiryDate"
/>
</template>
</AppLayout>
</template>
Loading
Loading