Basic editing works
This commit is contained in:
parent
b4eeb244bf
commit
520b3a6753
24 changed files with 448 additions and 47 deletions
|
@ -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": "/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
$block: &;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-items: stretch;
|
||||
height: auto;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
81
packages/example-site/components/Page.tsx
Normal file
81
packages/example-site/components/Page.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
37
packages/example-site/components/PageToolbar.tsx
Normal file
37
packages/example-site/components/PageToolbar.tsx
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
40
packages/example-site/composables/editor.ts
Normal file
40
packages/example-site/composables/editor.ts
Normal 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,
|
||||
};
|
||||
};
|
55
packages/example-site/composables/states.ts
Normal file
55
packages/example-site/composables/states.ts
Normal 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,
|
||||
};
|
||||
};
|
9
packages/example-site/middleware/authenticate.ts
Normal file
9
packages/example-site/middleware/authenticate.ts
Normal 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);
|
||||
});
|
26
packages/example-site/middleware/page.ts
Normal file
26
packages/example-site/middleware/page.ts
Normal 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);
|
||||
});
|
|
@ -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
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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 />;
|
||||
},
|
||||
});
|
||||
|
|
68
packages/example-site/pages/admin/login.tsx
Normal file
68
packages/example-site/pages/admin/login.tsx
Normal 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>);
|
||||
},
|
||||
});
|
|
@ -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 />;
|
||||
},
|
||||
});
|
||||
|
|
14
packages/example-site/queries/createPage.gql
Normal file
14
packages/example-site/queries/createPage.gql
Normal file
|
@ -0,0 +1,14 @@
|
|||
mutation createPage($data: PageInput!) {
|
||||
createPage(data: $data) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
title
|
||||
path
|
||||
block
|
||||
public
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
packages/example-site/queries/login.gql
Normal file
17
packages/example-site/queries/login.gql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
packages/example-site/queries/me.gql
Normal file
15
packages/example-site/queries/me.gql
Normal file
|
@ -0,0 +1,15 @@
|
|||
query me {
|
||||
me {
|
||||
id
|
||||
username
|
||||
blocked
|
||||
confirmed
|
||||
email
|
||||
role {
|
||||
id
|
||||
description
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
packages/example-site/queries/pages.gql
Normal file
14
packages/example-site/queries/pages.gql
Normal file
|
@ -0,0 +1,14 @@
|
|||
query pages($filters: PageFiltersInput) {
|
||||
pages(filters: $filters) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
title
|
||||
path
|
||||
block
|
||||
public
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
packages/example-site/queries/updatePage.gql
Normal file
9
packages/example-site/queries/updatePage.gql
Normal file
|
@ -0,0 +1,9 @@
|
|||
mutation updatePage($id: ID!, $data: PageInput!) {
|
||||
updatePage(id: $id, data: $data) {
|
||||
data {
|
||||
attributes {
|
||||
block
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
packages/example-site/utils/page.ts
Normal file
17
packages/example-site/utils/page.ts
Normal 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' }),
|
||||
}
|
||||
],
|
||||
}),
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue