Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

!!! FEATURE: Extract workspace metadata and user-assignment to Neos #5146

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
5f1e296
FEATURE: Extract workspace metadata and user-assignment to Neos
bwaidelich Jun 17, 2024
113e0b6
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Jun 28, 2024
773de1a
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Jul 4, 2024
8975a5f
wip
bwaidelich Jul 8, 2024
9c74b65
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Aug 2, 2024
4326424
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Aug 7, 2024
54df32a
Mini tweaks
bwaidelich Aug 9, 2024
dccfb50
wip
bwaidelich Aug 16, 2024
723f26c
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Sep 6, 2024
0af1fa2
WIP: Fix WorkspaceService as discussed with basti & christian
mhsdesign Sep 9, 2024
80e3aef
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Sep 10, 2024
e705924
Fix WorkspaceService::createRootWorkspace()
bwaidelich Sep 10, 2024
11aa9b3
Use `WorkspaceService` in workspace::createroot CLI command
bwaidelich Sep 10, 2024
bdfb954
Stabilize unique workspace name creation
bwaidelich Sep 10, 2024
cf95ee2
Merge remote-tracking branch 'origin/9.0' into feature/4726-extract-w…
mhsdesign Sep 17, 2024
0f20b89
Provide CLI to synchronize workspace metadata and roles
bwaidelich Sep 19, 2024
a467296
Stabilize and document WorkspaceService
bwaidelich Sep 23, 2024
616852e
Fix workspace role handling
bwaidelich Sep 25, 2024
5d448c8
Improve "pending changes" projection and read model
bwaidelich Sep 25, 2024
fd1a616
Cosmetic tweaks to satisfy linter
bwaidelich Sep 25, 2024
b70fb60
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Sep 25, 2024
fe34b60
Move owner assignment from role to metadata, fix migration
bwaidelich Sep 27, 2024
96c427b
Remove UI specific publishable node info array from `Changes` VO
bwaidelich Sep 27, 2024
c3f51e2
Make `WorkspaceNameBuilder` obsolete and remove it
bwaidelich Sep 27, 2024
732b81a
Remove workspace related left-overs from `UserService`
bwaidelich Sep 27, 2024
8f87ddf
Make `Utility\User` class obsolete and remove it
bwaidelich Sep 30, 2024
cbe3ba5
Rework migration to work on the database table directly
bwaidelich Sep 30, 2024
22d8d3a
TASK: Make migration more stable by not relying on the projection but…
mhsdesign Sep 30, 2024
1e1849d
Optimize `migrateWorkspaceMetadataToWorkspaceService` migration
bwaidelich Oct 1, 2024
a27fb02
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Oct 1, 2024
b84ae90
TASK: Adjust deprecations
mhsdesign Oct 1, 2024
702d749
TASK: Deprecate `UserInterface`
mhsdesign Oct 1, 2024
3410ca7
TASK: Add comment for `$Persistence_Object_Identifier` "hack"
mhsdesign Oct 1, 2024
686ff3d
TASK: Improve exceptions
mhsdesign Oct 1, 2024
5c3e817
TASK: Make migration independent of workspaceService
mhsdesign Oct 1, 2024
721b413
TASK: Add missing `@internal` annotations
mhsdesign Oct 1, 2024
e64ff93
TASK: Remove obsolete todos
mhsdesign Oct 1, 2024
88e344d
Merge branch '9.0' into feature/4726-extract-workspace-metadata-and-u…
bwaidelich Oct 7, 2024
25e9957
Rename `WorkspaceSubjectType` to `WorkspaceRoleSubjectType`
bwaidelich Oct 8, 2024
2dde2cf
Make `WorkspaceService` more explicit
bwaidelich Oct 8, 2024
1e3e2f7
Workspace UI: Workspace role preset
bwaidelich Oct 8, 2024
904c716
`WorkspacePermissions` doc comments
bwaidelich Oct 8, 2024
8bafb5a
Add more doc comments
bwaidelich Oct 8, 2024
f68300a
wip
bwaidelich Oct 8, 2024
7d6a819
Fix and improve Behat tests for WorkspaceService
bwaidelich Oct 9, 2024
3b3d718
Remove `workspaceName` from `WorkspaceMetadata`
bwaidelich Oct 9, 2024
e284cf0
Fix typo in private method
bwaidelich Oct 9, 2024
cc85cbf
TASK: Improve documentation
mhsdesign Oct 9, 2024
02d9c97
Merge branch 'feature/4726-extract-workspace-metadata-and-user-assign…
bwaidelich Oct 9, 2024
5fc33d5
Allow to determine Workspace role assignments
bwaidelich Oct 9, 2024
31119ae
Allow to determine Workspace role assignments
bwaidelich Oct 9, 2024
51c7201
stabilize WorkspaceMetadata constructor
bwaidelich Oct 9, 2024
aa1655d
Fix `WorkspaceService::getWorkspacePermissionsForUser()`
bwaidelich Oct 9, 2024
732acc8
Merge branch 'feature/4726-extract-workspace-metadata-and-user-assign…
bwaidelich Oct 9, 2024
5540c83
TASK: Use `WorkspaceRoleAssignment` in `assignWorkspaceRole` signatur
mhsdesign Oct 9, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,72 @@
*
* @api
*/
class Workspace
final readonly class Workspace
{
/**
* This prefix determines if a given workspace (name) is a user workspace.
*/
public const PERSONAL_WORKSPACE_PREFIX = 'user-';
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

/**
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved
* @var WorkspaceName Workspace identifier, unique within one Content Repository instance
*/
public WorkspaceName $workspaceName;

/**
* @var WorkspaceName|null Workspace identifier of the base workspace (i.e. the target when publishing changes) – if null this instance is considered a root (aka public) workspace
*/
public ?WorkspaceName $baseWorkspaceName;

/**
* @deprecated with 9.0.0-beta12 metadata should be assigned to workspaces outside the Content Repository core
*/
public WorkspaceTitle $workspaceTitle;

/**
* @deprecated with 9.0.0-beta12metadata should be assigned to workspaces outside the Content Repository core
*/
public WorkspaceDescription $workspaceDescription;

/**
* The Content Stream this workspace currently points to – usually it is set to a new, empty content stream after publishing/rebasing the workspace
*/
public ContentStreamId $currentContentStreamId;

/**
* The current status of this workspace
*/
public WorkspaceStatus $status;

/**
* @deprecated with 9.0.0-beta12 owners/collaborators should be assigned to workspaces outside the Content Repository core
*/
public string|null $workspaceOwner;
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

/**
* @internal
*/
public function __construct(
public readonly WorkspaceName $workspaceName,
public readonly ?WorkspaceName $baseWorkspaceName,
public readonly WorkspaceTitle $workspaceTitle,
public readonly WorkspaceDescription $workspaceDescription,
public readonly ContentStreamId $currentContentStreamId,
public readonly WorkspaceStatus $status,
public readonly ?string $workspaceOwner
WorkspaceName $workspaceName,
?WorkspaceName $baseWorkspaceName,
WorkspaceTitle $workspaceTitle,
WorkspaceDescription $workspaceDescription,
ContentStreamId $currentContentStreamId,
WorkspaceStatus $status,
?string $workspaceOwner
) {
$this->workspaceName = $workspaceName;
$this->baseWorkspaceName = $baseWorkspaceName;
$this->workspaceTitle = $workspaceTitle;
$this->workspaceDescription = $workspaceDescription;
$this->currentContentStreamId = $currentContentStreamId;
$this->status = $status;
$this->workspaceOwner = $workspaceOwner;
}


/**
* Checks if this workspace is a user's personal workspace
* @api
* @deprecated with 9.0.0-beta12 owners/collaborators should be assigned to workspaces outside the Content Repository core
*/
public function isPersonalWorkspace(): bool
{
Expand All @@ -59,7 +100,7 @@ public function isPersonalWorkspace(): bool
* Checks if this workspace is shared only across users with access to internal workspaces, for example "reviewers"
*
* @return bool
* @api
* @deprecated with 9.0.0-beta12 owners/collaborators should be assigned to workspaces outside the Content Repository core
*/
public function isPrivateWorkspace(): bool
{
Expand All @@ -70,6 +111,7 @@ public function isPrivateWorkspace(): bool
* Checks if this workspace is shared across all editors
*
* @return boolean
* @deprecated with 9.0.0-beta12 owners/collaborators should be assigned to workspaces outside the Content Repository core
*/
public function isInternalWorkspace(): bool
{
Expand Down
128 changes: 83 additions & 45 deletions Neos.Neos/Classes/Command/WorkspaceCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Dto\RebaseErrorHandlingStrategy;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Exception\WorkspaceRebaseFailed;
use Neos\ContentRepository\Core\Projection\Workspace\Workspace;
use Neos\ContentRepository\Core\Projection\Workspace\WorkspaceStatus;
use Neos\ContentRepository\Core\Service\WorkspaceMaintenanceServiceFactory;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;
use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist;
use Neos\ContentRepository\Core\SharedModel\User\UserId;
use Neos\ContentRepository\Core\SharedModel\Workspace\ContentStreamId;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceDescription;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
Expand All @@ -36,9 +34,11 @@
use Neos\Flow\Cli\CommandController;
use Neos\Flow\Cli\Exception\StopCommandException;
use Neos\Flow\Persistence\PersistenceManagerInterface;
use Neos\Neos\Domain\Model\UserId;
use Neos\Neos\Domain\Model\WorkspaceClassification;
use Neos\Neos\Domain\Service\UserService;
use Neos\Neos\Domain\Workspace\WorkspaceProvider;
use Neos\Neos\PendingChangesProjection\ChangeFinder;
use Neos\Neos\Domain\Service\WorkspacePublishingService;
use Neos\Neos\Domain\Service\WorkspaceService;

/**
* The Workspace Command Controller
Expand All @@ -56,7 +56,10 @@ class WorkspaceCommandController extends CommandController
protected ContentRepositoryRegistry $contentRepositoryRegistry;

#[Flow\Inject]
protected WorkspaceProvider $workspaceProvider;
protected WorkspacePublishingService $workspacePublishingService;

#[Flow\Inject]
protected WorkspaceService $workspaceService;

/**
* Publish changes of a workspace
Expand All @@ -68,16 +71,14 @@ class WorkspaceCommandController extends CommandController
*/
public function publishCommand(string $workspace, string $contentRepository = 'default'): void
{
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$this->workspacePublishingService->publishWorkspace(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);
$workspace->publishAllChanges();

$this->outputLine(
'Published all nodes in workspace %s to its base workspace',
[$workspace->name->value]
[$workspace]
);
}

Expand All @@ -92,19 +93,17 @@ public function publishCommand(string $workspace, string $contentRepository = 'd
*/
public function discardCommand(string $workspace, string $contentRepository = 'default'): void
{
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);

try {
$workspace->discardAllChanges();
// @todo: bypass access control
$this->workspacePublishingService->discardAllWorkspaceChanges(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
);
} catch (WorkspaceDoesNotExist $exception) {
$this->outputLine('Workspace "%s" does not exist', [$workspace->name->value]);
$this->outputLine('Workspace "%s" does not exist', [$workspace]);
$this->quit(1);
}
$this->outputLine('Discarded all nodes in workspace %s', [$workspace->name->value]);
$this->outputLine('Discarded all nodes in workspace %s', [$workspace]);
}

/**
Expand All @@ -121,11 +120,11 @@ public function rebaseCommand(string $workspace, string $contentRepository = 'de
{
try {
// @todo: bypass access control
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$this->workspacePublishingService->rebaseWorkspace(
ContentRepositoryId::fromString($contentRepository),
WorkspaceName::fromString($workspace)
WorkspaceName::fromString($workspace),
$force ? RebaseErrorHandlingStrategy::STRATEGY_FORCE : RebaseErrorHandlingStrategy::STRATEGY_FAIL,
);
$workspace->rebase($force ? RebaseErrorHandlingStrategy::STRATEGY_FORCE : RebaseErrorHandlingStrategy::STRATEGY_FAIL);
} catch (WorkspaceDoesNotExist $exception) {
$this->outputLine('Workspace "%s" does not exist', [$workspace]);
$this->quit(1);
Expand All @@ -134,7 +133,7 @@ public function rebaseCommand(string $workspace, string $contentRepository = 'de
$this->quit(1);
}

$this->outputLine('Rebased workspace %s', [$workspace->name->value]);
$this->outputLine('Rebased workspace %s', [$workspace]);
}

/**
Expand Down Expand Up @@ -178,13 +177,23 @@ public function createCommand(
string $contentRepository = 'default'
): void {
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);

$this->workspaceService->createWorkspace(
$contentRepositoryId,
\Neos\Neos\Domain\Model\WorkspaceTitle::fromString($title ?? $workspace),
\Neos\Neos\Domain\Model\WorkspaceDescription::fromString($description ?? ''),
WorkspaceName::fromString($baseWorkspace),
\Neos\Neos\Domain\Model\UserId::fromString($owner),
WorkspaceClassification::PERSONAL,
);

$contentRepositoryInstance = $this->contentRepositoryRegistry->get($contentRepositoryId);

if ($owner === '') {
$workspaceOwnerUserId = null;
} else {
$workspaceOwnerUserId = UserId::fromString($owner);
$workspaceOwner = $this->userService->findByUserIdentifier($workspaceOwnerUserId);
$workspaceOwner = $this->userService->findUserById($workspaceOwnerUserId);
bwaidelich marked this conversation as resolved.
Show resolved Hide resolved
if ($workspaceOwner === null) {
$this->outputLine('The user "%s" specified as owner does not exist', [$owner]);
$this->quit(3);
Expand All @@ -198,7 +207,7 @@ public function createCommand(
WorkspaceTitle::fromString($title ?: $workspace),
WorkspaceDescription::fromString($description ?: $workspace),
ContentStreamId::create(),
$workspaceOwnerUserId
$workspaceOwnerUserId !== null ? \Neos\ContentRepository\Core\SharedModel\User\UserId::fromString($workspaceOwnerUserId->value) : null,
));
} catch (WorkspaceAlreadyExists $workspaceAlreadyExists) {
$this->outputLine('Workspace "%s" already exists', [$workspace]);
Expand All @@ -208,7 +217,7 @@ public function createCommand(
$this->quit(2);
}

if ($workspaceOwnerUserId instanceof UserId) {
if ($workspaceOwnerUserId !== null) {
$this->outputLine(
'Created a new workspace "%s", based on workspace "%s", owned by "%s".',
[$workspace, $baseWorkspace, $owner]
Expand Down Expand Up @@ -243,13 +252,14 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
$this->quit(2);
}

$workspace = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName($workspaceName);
if (!$workspace instanceof Workspace) {
$crWorkspace = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName($workspaceName);
if ($crWorkspace === null) {
$this->outputLine('Workspace "%s" does not exist', [$workspaceName->value]);
$this->quit(1);
}
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName);

if ($workspace->isPersonalWorkspace()) {
if ($workspaceMetadata->classification === WorkspaceClassification::PERSONAL) {
$this->outputLine(
'Did not delete workspace "%s" because it is a personal workspace.'
. ' Personal workspaces cannot be deleted manually.',
Expand All @@ -264,28 +274,28 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
'Workspace "%s" cannot be deleted because the following workspaces are based on it:',
[$workspaceName->value]
);

$this->outputLine();
$tableRows = [];
$headerRow = ['Name', 'Title', 'Description'];

foreach ($dependentWorkspaces as $dependentWorkspace) {
$dependentWorkspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $dependentWorkspace->workspaceName);
$tableRows[] = [
$dependentWorkspace->workspaceName->value,
$dependentWorkspace->workspaceTitle->value,
$dependentWorkspace->workspaceDescription->value
$dependentWorkspaceMetadata->title->value,
$dependentWorkspaceMetadata->description->value
];
}
$this->output->outputTable($tableRows, $headerRow);
$this->quit(3);
}


try {
$nodesCount = $contentRepositoryInstance->projectionState(ChangeFinder::class)
->countByContentStreamId(
$workspace->currentContentStreamId
);
$nodesCount = $this->workspacePublishingService->countPendingWorkspaceChanges($contentRepositoryId, $workspaceName);
} catch (\Exception $exception) {
$this->outputLine('Could not fetch unpublished nodes for workspace %s, nothing was deleted. %s', [$workspace->workspaceName->value, $exception->getMessage()]);
$this->outputLine('Could not fetch unpublished nodes for workspace %s, nothing was deleted. %s', [$workspaceName->value, $exception->getMessage()]);
$this->quit(4);
}

Expand All @@ -299,11 +309,7 @@ public function deleteCommand(string $workspace, bool $force = false, string $co
$this->quit(5);
}
// @todo bypass access control?
$workspace = $this->workspaceProvider->provideForWorkspaceName(
$contentRepositoryId,
$workspaceName
);
$workspace->discardAllChanges();
$this->workspacePublishingService->discardAllWorkspaceChanges($contentRepositoryId, $workspaceName);
}

$contentRepositoryInstance->handle(
Expand Down Expand Up @@ -359,20 +365,52 @@ public function listCommand(string $contentRepository = 'default'): void
}

$tableRows = [];
$headerRow = ['Name', 'Base Workspace', 'Title', 'Owner', 'Description', 'Status', 'Content Stream'];
$headerRow = ['Name', 'Classification', 'Base Workspace', 'Title', 'Description', 'Status', 'Content Stream'];
mhsdesign marked this conversation as resolved.
Show resolved Hide resolved

foreach ($workspaces as $workspace) {
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspace->workspaceName);

/* @var Workspace $workspace */
$tableRows[] = [
$workspace->workspaceName->value,
$workspace->baseWorkspaceName?->value ?: '',
$workspace->workspaceTitle->value,
$workspace->workspaceOwner ?: '',
$workspace->workspaceDescription->value,
$workspaceMetadata->classification->value,
$workspace->baseWorkspaceName?->value ?: '-',
$workspaceMetadata->title->value,
$workspaceMetadata->description->value,
$workspace->status->value,
$workspace->currentContentStreamId->value,
];
}
$this->output->outputTable($tableRows, $headerRow);
}

/**
* Display details for the specified workspace
*
* @param string $workspace Name of the workspace to show
* @param string $contentRepository The name of the content repository. (Default: 'default')
* @throws StopCommandException
*/
public function showCommand(string $workspace, string $contentRepository = 'default'): void
{
$contentRepositoryId = ContentRepositoryId::fromString($contentRepository);
$contentRepositoryInstance = $this->contentRepositoryRegistry->get($contentRepositoryId);

$workspaceName = WorkspaceName::fromString($workspace);
$workspacesInstance = $contentRepositoryInstance->getWorkspaceFinder()->findOneByName($workspaceName);

if ($workspacesInstance === null) {
$this->outputLine('Workspace "%s" not found.', [$workspaceName->value]);
$this->quit();
}
$workspaceMetadata = $this->workspaceService->getWorkspaceMetadata($contentRepositoryId, $workspaceName);

$this->outputFormatted('Name: <b>%s</b>', [$workspacesInstance->workspaceName->value]);
$this->outputFormatted('Classification: <b>%s</b>', [$workspaceMetadata->classification->value]);
$this->outputFormatted('Base Workspace: <b>%s</b>', [$workspacesInstance->baseWorkspaceName?->value ?: '-']);
$this->outputFormatted('Title: <b>%s</b>', [$workspaceMetadata->title->value]);
$this->outputFormatted('Description: <b>%s</b>', [$workspaceMetadata->description->value]);
$this->outputFormatted('Status: <b>%s</b>', [$workspacesInstance->status->value]);
$this->outputFormatted('Content Stream: <b>%s</b>', [$workspacesInstance->currentContentStreamId->value]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

declare(strict_types=1);

namespace Neos\Neos\Domain\Workspace;
namespace Neos\Neos\Domain\Model;

use Neos\Flow\Annotations as Flow;

Expand Down
Loading
Loading