create person and relation ship on edge drop

This commit is contained in:
2025-04-29 21:37:50 +02:00
parent 5b2fd594f2
commit 3ffc12012f
8 changed files with 148 additions and 12 deletions

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import type { components } from '$lib/api/api.gen.ts';
import { child, spouse, parent, sibling } from '$lib/paraglide/messages';
import { getSmoothStepPath, BaseEdge, EdgeLabelRenderer, type EdgeProps } from '@xyflow/svelte';
let {
sourceX,
sourceY,
source,
sourcePosition,
target,
targetX,
targetY,
targetPosition,
markerEnd,
style,
data
}: EdgeProps = $props();
let [edgePath, labelX, labelY] = $derived(
getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
})
);
const onEdgeClick = () => {
window.dispatchEvent(
new CustomEvent('edge-click', {
detail: {
start: source,
end: target,
data: data as components['schemas']['FamilyRelationship'] & { type: string }
}
})
);
};
let edgeType = (
data as components['schemas']['FamilyRelationship'] & { type: string }
).type.toLowerCase();
let edgeLabel: string = $state(edgeType);
let edgeColor: string;
if (edgeType === 'spouse') {
edgeColor = 'stroke: red;';
edgeLabel = spouse();
} else if (edgeType === 'child') {
edgeColor = 'stroke: blue;';
edgeLabel = child();
} else if (edgeType === 'parent') {
edgeColor = 'stroke: green;';
edgeLabel = parent();
} else if (edgeType === 'sibling') {
edgeColor = 'stroke: brown;';
edgeLabel = sibling();
} else {
edgeColor = 'stroke: gray;';
edgeLabel = edgeType;
}
</script>
<BaseEdge path={edgePath} {markerEnd} {style} />
<EdgeLabelRenderer>
<div
class="button-edge__label nodrag nopan"
style:transform="translate(-50%, -50%) translate({labelX}px,{labelY}px)"
>
<button class="button-edge__button" onclick={onEdgeClick}>{edgeLabel}</button>
</div>
</EdgeLabelRenderer>

View File

@@ -7,7 +7,6 @@
type $$Props = NodeProps;
export let data: NodeProps['data'] & components['schemas']['PersonProperties'];
const connection = useConnection();
</script>
<div
@@ -16,6 +15,7 @@
<Handle
class="customHandle"
{isValidConnection}
isConnectable={true}
position={Position.Bottom}
type="source"
style="z-index: 1;"
@@ -25,11 +25,12 @@
class="customHandle"
{isValidConnection}
position={Position.Top}
isConnectable={true}
type="target"
isConnectableStart={false}
/>
<div class="avatar mb-2">
<div class="avatar mb-2" style="z-index: 2; cursor: pointer;">
<div
class="ring-accent ring-offset-accent bg-accent w-24 rounded-full border-0 ring ring-offset-1"
>

View File

@@ -34,8 +34,6 @@ export class FamilyTree extends dagre.graphlib.Graph {
edges.forEach((edge) => {
let newEdge = { ...edge };
if (edge.data?.type === 'spouse') {
newEdge.style = 'dashed; stroke: #000; stroke-width: 2px; color: red;';
const sourceNode = this.node(edge.source);
const targetNode = this.node(edge.target);
if (!sourceNode || !targetNode) {
@@ -76,7 +74,7 @@ export class FamilyTree extends dagre.graphlib.Graph {
targetNode.x = desiredX;
targetNode.y = sourceNode.y;
}
newEdge.type = 'smoothstep';
newEdge.type = 'familyEdge';
newEdges.push(newEdge);
});

View File

@@ -1,7 +1,11 @@
import type { Node, Edge, NodeTypes } from '@xyflow/svelte';
import type { Node, Edge, NodeTypes, EdgeTypes } from '@xyflow/svelte';
import FamilyEdge from './FamilyEdge.svelte';
import PersonNode from './PersonNode.svelte';
export const nodeTypes: NodeTypes = { personNode: PersonNode };
export const edgeTypes: EdgeTypes = {
familyEdge: FamilyEdge
};
export type NodeMenu = {
onClick: () => void;

View File

@@ -35,7 +35,7 @@
closeModal = () => {},
onCreation = (nodes: Array<Node> | null, edges: Array<Edge> | null) => {},
onOnlyPersonCreation = (person: components['schemas']['Person']) => {},
relationshipStartID = null
relationshipStartID
}: {
closeModal: () => void;
onCreation: (newNodes: Array<Node> | null, newEdges: Array<Edge> | null) => void;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { nodeTypes } from '$lib/graph/model';
import { nodeTypes, edgeTypes } from '$lib/graph/model';
import { title, family_tree } from '$lib/paraglide/messages.js';
import {
@@ -7,10 +7,10 @@
SvelteFlow,
Controls,
MiniMap,
ConnectionLineType
ConnectionLineType,
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import type { Node, Edge, NodeEventWithPointer } from '@xyflow/svelte';
import type {OnConnectEnd, Node, Edge, NodeEventWithPointer } from '@xyflow/svelte';
import PersonModal from '$lib/profile/Modal.svelte';
import PersonMenu from '$lib/graph/PersonMenu.svelte';
@@ -173,8 +173,17 @@
let handlePaneClick = ({ event }: { event: MouseEvent }) => {
openPersonPanel = false;
openPersonMenu = undefined;
relationshipStart = null;
};
const handleConnectEnd: OnConnectEnd = (event, connectionState) => {
if (connectionState.isValid) return;
const sourceNodeId = connectionState.fromNode?.id
if (sourceNodeId === undefined) return;
relationshipStart = Number(sourceNodeId);
createPerson = true;
console.log('createPerson', createPerson);
console.log('relationshipStart', relationshipStart);
}
</script>
<svelte:head>
@@ -186,11 +195,13 @@
<SvelteFlow
bind:nodes
bind:edges
onconnectend={handleConnectEnd}
onnodeclick={handleNodeClickFunc}
onnodecontextmenu={handleContextMenu}
onpaneclick={handlePaneClick}
class="!bg-base-200"
{nodeTypes}
{edgeTypes}
fitView
onlyRenderVisibleElements
connectionLineType={ConnectionLineType.SmoothStep}

View File

@@ -29,6 +29,12 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
return {};
}
let already_loaded = event.cookies.get('already_loaded') ?? null;
if (already_loaded !== null) {
return {};
}
const storedState = event.cookies.get('google_oauth_state') ?? null;
const codeVerifier = event.cookies.get('google_code_verifier') ?? null;
const code = event.url.searchParams.get('code');
@@ -95,6 +101,14 @@ export const load: PageServerLoad = async (event: RequestEvent) => {
email: email
};
event.cookies.set('already_loaded', 'true',{
path: '/login/google/callback',
sameSite: 'lax',
httpOnly: true,
maxAge: 60 * 10,
secure: import.meta.env.PROD,
})
return {
props: personP
};
@@ -110,13 +124,26 @@ async function register(event: RequestEvent) {
}
const data = await event.request.formData();
let parsedData: components['schemas']['PersonRegistration'] = {
first_name: data.get('first_name'),
last_name: data.get('last_name'),
email: data.get('email'),
biological_sex: data.get('biological_sex'),
born: data.get('birth_date'),
mothers_first_name: data.get('mothers_first_name'),
mothers_last_name: data.get('mothers_last_name'),
google_id: data.get('google_id'),
limit: StorageLimit,
} as components['schemas']['PersonRegistration'];
if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) {
return fail(500, { message: 'Server configuration error. GH_SESSIONS KeyValue store missing' });
return fail(500, { data: parsedData, message: 'Server configuration error. GH_SESSIONS KeyValue store missing' });
}
const first_name_f = data.get('first_name');
if (first_name_f === null || first_name_f === '') {
return fail(400, {
data:parsedData,
message: missing_field({
field: first_name()
})
@@ -126,6 +153,7 @@ async function register(event: RequestEvent) {
const google_id = data.get('google_id');
if (google_id === null || google_id === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: 'google_id'
})
@@ -135,6 +163,7 @@ async function register(event: RequestEvent) {
const last_name_f = data.get('last_name');
if (last_name_f === null || last_name_f === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: last_name()
})
@@ -161,6 +190,7 @@ async function register(event: RequestEvent) {
const bbiological_sex = data.get('biological_sex');
if (bbiological_sex === null || bbiological_sex === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: biological_sex()
})
@@ -169,6 +199,7 @@ async function register(event: RequestEvent) {
!['male', 'female', 'intersex', 'unknown', 'other'].includes(bbiological_sex.toString())
) {
return fail(400, {
data: parsedData,
message: `Invalid value for biological_sex. Must be one of "male", "female", "intersex", "unknown", or "other".`
});
}
@@ -176,6 +207,7 @@ async function register(event: RequestEvent) {
const mothers_first_name_f = data.get('mothers_first_name');
if (mothers_first_name_f === null || mothers_first_name_f === '') {
return fail(400, {
data: parsedData,
message: missing_field({
field: mothers_first_name()
})
@@ -184,6 +216,7 @@ async function register(event: RequestEvent) {
const mothers_last_name_f = data.get('mothers_last_name');
if (mothers_last_name_f === null) {
return fail(400, {
data: parsedData,
message: missing_field({
field: mothers_last_name()
})
@@ -205,6 +238,7 @@ async function register(event: RequestEvent) {
let response = await client.POST('/person/google/{google_id}', {
params: {
data: parsedData,
path: { google_id: google_id.toString() }
},
body: personP
@@ -212,24 +246,28 @@ async function register(event: RequestEvent) {
if (response.response.status !== 200) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + response.error?.msg
});
}
if (response.data === undefined) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + 'No user data returned'
});
}
if (response.data.Id === undefined) {
return fail(400, {
data: parsedData,
message: failed_to_create_user() + 'No user ID returned'
});
}
if (!event.platform) {
return fail(500, {
data: parsedData,
message: 'Server configuration error. GH_SESSIONS KeyValue store missing'
});
}
@@ -242,11 +280,21 @@ async function register(event: RequestEvent) {
);
if (session === null) {
return fail(500, {
data: parsedData,
message: failed_to_create_user() + 'Failed to create session'
});
}
setSessionTokenCookie(event, sessionToken, session.expiresAt);
event.cookies.delete('already_loaded',
{
path: '/login/google/callback',
sameSite: 'lax',
httpOnly: true,
maxAge: 0,
secure: import.meta.env.PROD
}
);
return redirect(302, '/');
}