Basic editing works

cms
Benjamin Bädorf 2022-12-28 19:46:51 +01:00
parent b4eeb244bf
commit 520b3a6753
No known key found for this signature in database
GPG Key ID: 4406E80E13CD656C
24 changed files with 448 additions and 47 deletions

View File

@ -4,14 +4,15 @@
"info": {
"singularName": "page",
"pluralName": "pages",
"displayName": "Page"
"displayName": "Page",
"description": ""
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"Title": {
"title": {
"type": "string"
},
"block": {
@ -21,6 +22,13 @@
"type": "boolean",
"default": true,
"required": false
},
"path": {
"type": "string",
"required": true,
"regex": "^(\\/|(\\/[A-z0-9\\-]+)+)$",
"unique": true,
"default": "/"
}
}
}

View File

@ -2,6 +2,7 @@
$block: &;
display: flex;
flex-direction: column;
align-items: stretch;
justify-items: stretch;
height: auto;

View File

@ -59,11 +59,6 @@ export function useBlockTree() {
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
}
if (mode.value !== SbMode.Edit) {
console.warn('Ignoring block tree registration requests outside of edit mode.');
return;
}
self.id = block.id;
self.name = block.name;

View File

@ -0,0 +1,81 @@
import { defineComponent } from 'vue';
import { SbMain, SbMode } from '@schlechtenburg/core';
import PageToolbar from '~~/components/PageToolbar';
import SbLayout from '@schlechtenburg/layout';
import SbHeading from '@schlechtenburg/heading';
import SbParagraph from '@schlechtenburg/paragraph';
import SbImage from '@schlechtenburg/image';
export default defineComponent({
async setup() {
const { me } = useMe();
const loggedIn = computed(() => !!me.value?.id);
const { page, setPage } = usePage();
const block = page.value?.attributes?.block;
const {
mode,
draft,
updateDraft,
revision,
} = useEditor();
watch(revision, async () => {
const { data, error } = await useAsyncGql(
'updatePage',
{
id: page.value?.id || '',
data: {
block: draft.value!
},
},
);
if (error.value) {
console.error('Error updating page!');
console.error('error:', error.value);
console.error('data:', data.value);
return;
}
setPage(data.value?.updatePage?.data?.attributes?.block);
updateDraft(data.value?.updatePage?.data?.attributes?.block);
});
watch(mode, async (newMode) => {
if (newMode === SbMode.View) {
updateDraft(block!);
}
});
updateDraft(block!);
if (!block) {
console.error('No block!');
console.error('page', page.value);
}
return () => (
<div class="ex-page">
{loggedIn.value ? <PageToolbar></PageToolbar> : null}
{draft.value
? <SbMain
class="ex-page"
mode={mode.value}
eventUpdate={(updatedBlock) => updateDraft(updatedBlock)}
block={draft.value}
availableBlocks={[
SbLayout,
SbHeading,
SbParagraph,
SbImage,
]}
/>
: <div class="ex-page ex-page_corrupt">Corrupt page: {page.value?.attributes?.path} ({page.value?.id})</div>}
</div>
);
},
});

View File

@ -0,0 +1,37 @@
import { defineComponent } from 'vue';
import { SbMode } from '@schlechtenburg/core';
export default defineComponent({
async setup() {
const { me } = useMe();
const { page } = usePage();
const {
mode,
edit,
cancel,
save,
} = useEditor();
return () => (
<div class="ex-page-toolbar">
{ mode.value === SbMode.View
? <button
type="button"
onClick={() => edit(page.value?.attributes?.block!)}
>Edit</button>
: <>
<button
type="button"
onClick={() => cancel()}
>Cancel</button>
<button
type="button"
onClick={() => save()}
>Save</button>
</>}
</div>
);
},
});

View File

@ -0,0 +1,40 @@
import { IBlockData, SbMode } from "@schlechtenburg/core";
export const useEditor = () => {
const mode = useState<SbMode>('mode', () => SbMode.View);
const revision = useState<number>('revision', () => 0);
const draft = useState<IBlockData<any>|null>('draft', () => null);
const setMode = (newMode: SbMode) => {
mode.value = newMode;
};
const updateDraft = (newDraft: IBlockData<any>) => {
draft.value = newDraft;
}
const edit = (block: IBlockData<any>) => {
draft.value = block;
mode.value = SbMode.Edit;
};
const save = () => {
revision.value = revision.value + 1;
mode.value = SbMode.View;
};
const cancel = () => {
mode.value = SbMode.View;
};
return {
mode,
setMode,
edit,
cancel,
save,
revision,
draft,
updateDraft,
};
};

View File

@ -0,0 +1,55 @@
import { IBlockData } from "@schlechtenburg/core";
export interface IRole {
id?: string|null;
attributes?: {
description: string;
name: string;
};
}
export interface IUser {
id?: string|null;
attributes?: {
blocked?: boolean;
confirmed?: boolean;
email?: string;
role?: IRole;
username?: string;
};
}
export const useMe = () => {
const me = useState<IUser|null>('me', () => null);
const setMe = (user: IUser|null) => {
me.value = user;
};
return {
me,
setMe,
};
};
export interface IPage {
id?: string|null;
attributes?: {
title?: string;
block?: IBlockData<any>|null;
path?: string;
};
}
export const usePage = () => {
const page = useState<IPage|null>('page', () => null);
const setPage = (newPage: IPage|null) => {
page.value = newPage;
};
return {
page,
setPage,
};
};

View File

@ -0,0 +1,9 @@
import { IUser } from "~~/composables/states";
export default defineNuxtRouteMiddleware(async () => {
const { me, setMe } = useMe();
const { data } = await useAsyncGql('me');
setMe((data.value?.me as IUser) || null);
});

View File

@ -0,0 +1,26 @@
import { IPage } from "~~/composables/states";
export default defineNuxtRouteMiddleware(async (to, from) => {
const { setPage } = usePage();
const { data, error } = await useAsyncGql({
operation: 'pages',
variables: {
filters: { path: { eq: to.path }},
},
});
if (error.value) {
console.error('Error getting pages!');
console.error(error.value);
return;
}
const newPage = (data.value?.pages?.data[0] as IPage) || null;
if (newPage?.attributes && !newPage?.attributes?.block) {
newPage.attributes.block = getNewPageBlock();
}
setPage(newPage);
});

View File

@ -5,7 +5,7 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
GQL_HOST: 'http://localhost:1337/graphql' // overwritten by process.env.GQL_HOST
GQL_HOST: 'http://localhost:1337/graphql', // overwritten by process.env.GQL_HOST
},
},
});

View File

@ -1,21 +1,12 @@
import { defineComponent } from 'vue';
import {
IBlockData,
SbMain,
} from '@schlechtenburg/core';
import Page from '~~/components/Page';
export default defineComponent({
setup() {
const route = useRoute();
const path = route.path;
async setup() {
definePageMeta({
middleware: ['authenticate', 'page'],
});
console.log(path);
return () => (
<div class="sb-main-menu">
Path {path}
{/*<SbMain block={props.block} />*/}
</div>
);
return () => <Page />;
},
});

View File

@ -0,0 +1,68 @@
import { defineComponent } from 'vue';
export default defineComponent({
async setup() {
definePageMeta({
middleware: ['authenticate'],
});
const credentials = useState(() => ({
identifier: '',
password: '',
}));
const onSubmit = async (event:Event) => {
event.preventDefault();
const { data, error } = await useAsyncGql('login', credentials.value);
const { setMe } = useMe();
if (error.value) {
console.error('Failed to log in!');
console.error('error:', error.value);
console.error('data:', data.value);
setMe(null);
return;
}
useGqlToken({
token: data.value?.login?.jwt || null,
config: { type: 'Bearer' },
});
setMe(data.value?.login?.user || null);
navigateTo('/');
};
return () => (<div class="sbcms-admin">
<form
onSubmit={onSubmit}
>
<h1>Login</h1>
<label>
Username
<input
type="text"
value={credentials.value.identifier}
onChange={(event: Event) => {
credentials.value.identifier = (event.currentTarget as HTMLInputElement).value;
}}
/>
</label>
<label>
Password
<input
type="password"
value={credentials.value.password}
onChange={(event: Event) => {
credentials.value.password = (event.currentTarget as HTMLInputElement).value;
}}
/>
</label>
<button
type="submit"
>Log in</button>
</form>
</div>);
},
});

View File

@ -1,19 +1,12 @@
import {
defineComponent,
PropType,
} from 'vue';
import {
IBlockData,
SbMain,
} from '@schlechtenburg/core';
import { defineComponent } from 'vue';
import Page from '~~/components/Page';
export default defineComponent({
setup() {
return () => (
<div class="sb-main-menu">
Homepage
{/*<SbMain block={props.block} />*/}
</div>
);
async setup() {
definePageMeta({
middleware: ['authenticate', 'page'],
});
return () => <Page />;
},
});

View File

@ -0,0 +1,14 @@
mutation createPage($data: PageInput!) {
createPage(data: $data) {
data {
id
attributes {
title
path
block
public
publishedAt
}
}
}
}

View File

@ -0,0 +1,17 @@
mutation login($identifier: String!, $password: String!) {
login(input: { identifier: $identifier, password: $password }) {
jwt
user {
id
username
blocked
confirmed
email
role {
id
description
name
}
}
}
}

View File

@ -0,0 +1,15 @@
query me {
me {
id
username
blocked
confirmed
email
role {
id
description
name
}
}
}

View File

@ -1,7 +1,14 @@
query launches($limit: Int = 5) {
launches(limit: $limit) {
id
launch_year
mission_name
query page($id: ID) {
page(id: $id) {
data {
id
attributes {
title
path
block
public
publishedAt
}
}
}
}

View File

@ -0,0 +1,14 @@
query pages($filters: PageFiltersInput) {
pages(filters: $filters) {
data {
id
attributes {
title
path
block
public
publishedAt
}
}
}
}

View File

@ -0,0 +1,9 @@
mutation updatePage($id: ID!, $data: PageInput!) {
updatePage(id: $id, data: $data) {
data {
attributes {
block
}
}
}
}

View File

@ -0,0 +1,17 @@
import { generateBlockId, IBlockData } from "@schlechtenburg/core";
import { getDefaultData as getDefaultLayoutData, ILayoutData, name as layoutName } from "@schlechtenburg/layout";
import { getDefaultData as getDefaultHeadingData, name as headingName } from "@schlechtenburg/heading";
export const getNewPageBlock: () => IBlockData<ILayoutData> = () => ({
id: generateBlockId(),
name: layoutName,
data: getDefaultLayoutData({
children: [
{
id: generateBlockId(),
name: headingName,
data: getDefaultHeadingData({ value: 'New page' }),
}
],
}),
});

View File

@ -4,8 +4,9 @@ export interface IHeadingData {
level: number;
}
export const getDefaultData: () => IHeadingData = () => ({
export const getDefaultData: (data?: Partial<IHeadingData>) => IHeadingData = (data = {}) => ({
value: '',
align: 'left',
level: 1,
...data,
});

View File

@ -14,7 +14,7 @@ export interface IImageData {
description: IBlockData<IParagraphData>;
}
export const getDefaultData: () => IImageData = () => ({
export const getDefaultData: (data?: Partial<IImageData>) => IImageData = (data = {}) => ({
src: '',
alt: '',
description: {
@ -22,4 +22,5 @@ export const getDefaultData: () => IImageData = () => ({
name: paragraphName,
data: getDefaultParagraphData(),
},
...data,
});

View File

@ -5,7 +5,8 @@ export interface ILayoutData {
children: IBlockData<any>[];
}
export const getDefaultData: () => ILayoutData = () => ({
export const getDefaultData: (data?: Partial<ILayoutData>) => ILayoutData = (data = {}) => ({
orientation: 'vertical',
children: [],
...data,
});

View File

@ -3,7 +3,8 @@ export interface IParagraphData {
align: string;
}
export const getDefaultData: () => IParagraphData = () => ({
export const getDefaultData: (data?: Partial<IParagraphData>) => IParagraphData = (data = {}) => ({
value: '',
align: 'left',
...data,
});