Fix node collaborators management

This commit is contained in:
Hakan Shehu
2024-10-27 17:29:53 +01:00
parent 8ca2821a04
commit 1dda26de3e
4 changed files with 185 additions and 58 deletions

View File

@@ -47,7 +47,9 @@ export class NodeCollaboratorCreateMutationHandler
}
const collaboratorsMap = attributesMap.get('collaborators') as Y.Map<any>;
collaboratorsMap.set(input.userId, input.role);
input.collaboratorIds.forEach((collaboratorId) => {
collaboratorsMap.set(collaboratorId, input.role);
});
});
const attributes = JSON.stringify(attributesMap.toJSON());

View File

@@ -6,36 +6,60 @@ import {
QueryResult,
} from '@/operations/queries';
import { sql } from 'kysely';
import { NodeTypes } from '@/lib/constants';
import {
InheritNodeCollaboratorsGroup,
LocalNodeAttributes,
NodeCollaboratorNode,
NodeCollaboratorsWrapper,
} from '@/types/nodes';
import { MutationChange } from '@/operations/mutations';
import { isEqual } from 'lodash';
type NodeCollaboratorRow = {
node_id: string;
node_level: number;
node_attributes: string;
node_parent_id: string | null;
collaborator_id: string;
collaborator_attributes: string;
type NodeRow = {
id: string;
attributes: string;
parent_id: string | null;
level: number;
};
type NodeCollaboratorWithRole = {
nodeId: string;
collaboratorId: string;
role: string;
};
type CollaboratorNodeRow = {
id: string;
attributes: string;
};
export class NodeCollaboratorListQueryHandler
implements QueryHandler<NodeCollaboratorListQueryInput>
{
public async handleQuery(
input: NodeCollaboratorListQueryInput,
): Promise<QueryResult<NodeCollaboratorListQueryInput>> {
const rows = await this.fetchCollaborators(input);
const nodes = await this.fetchAncestors(input);
const collaboratorsWithRoles = this.extractCollaborators(nodes);
const collaboratorIds = collaboratorsWithRoles.map(
(collaborator) => collaborator.collaboratorId,
);
const collaboratorNodes = await this.fetchCollaborators(
input,
collaboratorIds,
);
return {
output: this.buildNodeCollaborators(input.nodeId, rows),
output: this.buildNodeCollaborators(
input.nodeId,
nodes,
collaboratorNodes,
collaboratorsWithRoles,
),
state: {
rows,
nodes,
collaboratorsWithRoles,
collaboratorNodes,
},
};
}
@@ -58,8 +82,21 @@ export class NodeCollaboratorListQueryHandler
};
}
const rows = await this.fetchCollaborators(input);
if (isEqual(rows, state.rows)) {
const nodes = await this.fetchAncestors(input);
const collaboratorsWithRoles = this.extractCollaborators(nodes);
const collaboratorIds = collaboratorsWithRoles.map(
(collaborator) => collaborator.collaboratorId,
);
const collaboratorNodes = await this.fetchCollaborators(
input,
collaboratorIds,
);
if (
isEqual(nodes, state.nodes) &&
isEqual(collaboratorsWithRoles, state.collaboratorsWithRoles) &&
isEqual(collaboratorNodes, state.collaboratorNodes)
) {
return {
hasChanges: false,
};
@@ -68,77 +105,137 @@ export class NodeCollaboratorListQueryHandler
return {
hasChanges: true,
result: {
output: this.buildNodeCollaborators(input.nodeId, rows),
output: this.buildNodeCollaborators(
input.nodeId,
nodes,
collaboratorNodes,
collaboratorsWithRoles,
),
state: {
rows,
nodes,
collaboratorsWithRoles,
collaboratorNodes,
},
},
};
}
private async fetchCollaborators(
private async fetchAncestors(
input: NodeCollaboratorListQueryInput,
): Promise<NodeCollaboratorRow[]> {
): Promise<NodeRow[]> {
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(
input.userId,
);
const query = sql<NodeCollaboratorRow>`
WITH RECURSIVE ancestors(id, parent_id, level) AS (
SELECT id, parent_id, 0 AS level
const query = sql<NodeRow>`
WITH RECURSIVE ancestors(id, attributes, parent_id, level) AS (
SELECT id, attributes, parent_id, 0 AS level
FROM nodes
WHERE id = ${input.nodeId}
UNION ALL
SELECT n.id, n.parent_id, a.level + 1
SELECT n.id, n.attributes, n.parent_id, a.level + 1
FROM nodes n
INNER JOIN ancestors a ON n.id = a.parent_id
)
SELECT
nc.node_id,
a.level AS node_level,
nn.attributes AS node_attributes,
nn.parent_id AS node_parent_id,
nc.collaborator_id,
cn.attributes AS collaborator_attributes,
nc.role
FROM node_collaborators nc
JOIN ancestors a ON nc.node_id = a.id
JOIN nodes nn ON nc.node_id = nn.id
JOIN nodes cn ON nc.collaborator_id = cn.id
WHERE cn.type = ${NodeTypes.User};
SELECT id, attributes, parent_id, level
FROM ancestors
ORDER BY level DESC;
`.compile(workspaceDatabase);
const result = await workspaceDatabase.executeQuery(query);
return result.rows;
}
private extractCollaborators(nodes: NodeRow[]): NodeCollaboratorWithRole[] {
const extractedCollaborators: NodeCollaboratorWithRole[] = [];
for (const node of nodes) {
const attributes = JSON.parse(node.attributes) as LocalNodeAttributes;
const collaborators = attributes.collaborators;
if (!collaborators) {
continue;
}
const collaboratorIds = Object.keys(collaborators);
for (const collaboratorId of collaboratorIds) {
extractedCollaborators.push({
nodeId: node.id,
collaboratorId,
role: collaborators[collaboratorId],
});
}
}
return extractedCollaborators;
}
private async fetchCollaborators(
input: NodeCollaboratorListQueryInput,
collaboratorIds: string[],
): Promise<CollaboratorNodeRow[]> {
if (collaboratorIds.length === 0) {
return [];
}
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(
input.userId,
);
const result = await workspaceDatabase
.selectFrom('nodes')
.select(['id', 'attributes'])
.where('id', 'in', collaboratorIds)
.execute();
return result;
}
private buildNodeCollaborators = (
nodeId: string,
rows: NodeCollaboratorRow[],
nodes: NodeRow[],
collaboratorNodes: CollaboratorNodeRow[],
collaboratorsWithRoles: NodeCollaboratorWithRole[],
): NodeCollaboratorsWrapper => {
const direct: NodeCollaboratorNode[] = [];
const inheritCollaboratorMap: Map<string, NodeCollaboratorRow> = new Map();
const inheritCollaboratorMap: Map<string, NodeCollaboratorWithRole> =
new Map();
for (const row of rows) {
if (row.node_id === nodeId) {
const collaboratorAttributes = JSON.parse(row.collaborator_attributes);
for (const collaboratorWithRole of collaboratorsWithRoles) {
if (collaboratorWithRole.nodeId === nodeId) {
const collaboratorAttributes = JSON.parse(
collaboratorNodes.find(
(row) => row.id === collaboratorWithRole.collaboratorId,
)?.attributes,
);
const collaborator: NodeCollaboratorNode = {
id: row.collaborator_id,
id: collaboratorWithRole.collaboratorId,
name: collaboratorAttributes.name,
email: collaboratorAttributes.email,
avatar: collaboratorAttributes.avatar || null,
role: row.role,
role: collaboratorWithRole.role,
};
direct.push(collaborator);
} else {
if (!inheritCollaboratorMap.has(row.collaborator_id)) {
inheritCollaboratorMap.set(row.collaborator_id, row);
if (!inheritCollaboratorMap.has(collaboratorWithRole.collaboratorId)) {
inheritCollaboratorMap.set(collaboratorWithRole.collaboratorId, {
nodeId: collaboratorWithRole.nodeId,
collaboratorId: collaboratorWithRole.collaboratorId,
role: collaboratorWithRole.role,
});
} else {
const existingRow = inheritCollaboratorMap.get(row.collaborator_id);
if (!existingRow || existingRow.node_level < row.node_level) {
inheritCollaboratorMap.set(row.collaborator_id, row);
const existingRow = inheritCollaboratorMap.get(
collaboratorWithRole.collaboratorId,
);
if (
!existingRow ||
existingRow.nodeId !== collaboratorWithRole.nodeId
) {
inheritCollaboratorMap.set(
collaboratorWithRole.collaboratorId,
collaboratorWithRole,
);
}
}
}
@@ -147,25 +244,49 @@ export class NodeCollaboratorListQueryHandler
const inheritLeveledMap: Map<number, InheritNodeCollaboratorsGroup> =
new Map();
for (const row of inheritCollaboratorMap.values()) {
if (!inheritLeveledMap.has(row.node_level)) {
const nodeAttributes = JSON.parse(row.node_attributes);
inheritLeveledMap.set(row.node_level, {
id: row.node_id,
for (const collaboratorWithRole of inheritCollaboratorMap.values()) {
if (
direct.some(
(collaborator) =>
collaborator.id === collaboratorWithRole.collaboratorId,
)
) {
continue;
}
const nodeLevel = nodes.find(
(node) => node.id === collaboratorWithRole.nodeId,
)?.level;
if (nodeLevel === undefined) {
continue;
}
if (!inheritLeveledMap.has(nodeLevel)) {
const nodeAttributes = JSON.parse(
nodes.find((node) => node.id === collaboratorWithRole.nodeId)
?.attributes,
);
inheritLeveledMap.set(nodeLevel, {
id: collaboratorWithRole.nodeId,
name: nodeAttributes.name,
avatar: nodeAttributes.avatar,
collaborators: [],
});
}
const group = inheritLeveledMap.get(row.node_level);
const collaboratorAttributes = JSON.parse(row.collaborator_attributes);
const group = inheritLeveledMap.get(nodeLevel);
const collaboratorAttributes = JSON.parse(
collaboratorNodes.find(
(row) => row.id === collaboratorWithRole.collaboratorId,
)?.attributes,
);
group.collaborators.push({
id: row.collaborator_id,
id: collaboratorWithRole.collaboratorId,
name: collaboratorAttributes.name,
avatar: collaboratorAttributes.avatar,
email: collaboratorAttributes.email,
role: row.role,
role: collaboratorWithRole.role,
});
}

View File

@@ -75,6 +75,10 @@ export class NodeCollaboratorSearchQueryHandler
private async fetchNodes(
input: NodeCollaboratorSearchQueryInput,
): Promise<SelectNode[]> {
if (input.searchQuery.length === 0) {
return [];
}
const workspaceDatabase = await databaseManager.getWorkspaceDatabase(
input.userId,
);

View File

@@ -5,7 +5,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/renderer/components/ui/dropdown-menu';
import { Check, ChevronsDown } from 'lucide-react';
import { Check, ChevronDown } from 'lucide-react';
interface NodeCollaboratorRole {
name: string;
@@ -51,7 +51,7 @@ export const NodeCollaboratorRoleDropdown = ({
<DropdownMenuTrigger asChild>
<p className="flex cursor-pointer flex-row items-center p-1 text-sm text-muted-foreground hover:bg-gray-50">
{currentRole?.name}
<ChevronsDown className="ml-2 size-4 text-muted-foreground" />
<ChevronDown className="ml-2 size-4 text-muted-foreground" />
</p>
</DropdownMenuTrigger>
<DropdownMenuContent>