Assets as document: replace snapshot with publish workflow
Remove assets from the snapshot system and replace with a publish-based document workflow that generates versioned ProseMirror documents. - Remove snapshot_id/source_id from asset and asset_vendor models - Delete AssetFilter (no longer needed without snapshot filtering) - Add PublishAssetList service, GraphQL mutation, MCP tool, CLI command, and n8n operation - Add asset_list_document_id column to generated_documents table - Generate ProseMirror documents with asset inventory tables (name, type, amount, data types stored, owner, vendors) - Add AssetListDocument resolver on Organization type - Update frontend to remove snapshot routes/params and add publish dialog - Add e2e tests for asset publish (immediate, with approvers, reuse, RBAC) - Add migration script for converting legacy asset snapshots to documents - Exclude ASSETS from snapshot type lists and e2e snapshot tests - Move generated_documents SQL to coredata methods on Datum and Asset - Clear generated document and SOA references on soft delete and archive Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -16,7 +16,6 @@ import { faviconUrl, getAssetTypeVariant } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Avatar, Badge, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
|
||||
import type { usePaginationFragmentHookType } from "react-relay/relay-hooks/usePaginationFragment";
|
||||
import { useParams } from "react-router";
|
||||
import type { OperationType } from "relay-runtime";
|
||||
|
||||
import type {
|
||||
@@ -66,17 +65,10 @@ export function ReadOnlyAssetsTable(props: Props) {
|
||||
function AssetRow({ entry }: { entry: AssetEntry }) {
|
||||
const organizationId = useOrganizationId();
|
||||
const { __ } = useTranslate();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const vendors = entry.vendors?.edges.map(edge => edge.node) ?? [];
|
||||
|
||||
return (
|
||||
<Tr
|
||||
to={
|
||||
snapshotId
|
||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/assets/${entry.id}`
|
||||
: `/organizations/${organizationId}/assets/${entry.id}`
|
||||
}
|
||||
>
|
||||
<Tr to={`/organizations/${organizationId}/assets/${entry.id}`}>
|
||||
<Td>{entry.name}</Td>
|
||||
<Td>
|
||||
<Badge variant={getAssetTypeVariant(entry.assetType)}>
|
||||
|
||||
@@ -18,16 +18,28 @@ import { useConfirm } from "@probo/ui";
|
||||
import { useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import { useMutationWithToasts } from "../useMutationWithToasts";
|
||||
import type {
|
||||
AssetGraphCreateMutation,
|
||||
AssetType,
|
||||
} from "#/__generated__/core/AssetGraphCreateMutation.graphql";
|
||||
import type { AssetGraphDeleteMutation } from "#/__generated__/core/AssetGraphDeleteMutation.graphql";
|
||||
import type { AssetGraphUpdateMutation } from "#/__generated__/core/AssetGraphUpdateMutation.graphql";
|
||||
|
||||
/* eslint-disable relay/unused-fields, relay/must-colocate-fragment-spreads */
|
||||
|
||||
export const assetsQuery = graphql`
|
||||
query AssetGraphListQuery($organizationId: ID!, $snapshotId: ID) {
|
||||
query AssetGraphListQuery($organizationId: ID!) {
|
||||
node(id: $organizationId) {
|
||||
... on Organization {
|
||||
canCreateAsset: permission(action: "core:asset:create")
|
||||
...AssetsPageFragment @arguments(snapshotId: $snapshotId)
|
||||
canPublishAssets: permission(action: "core:asset:publish")
|
||||
assetListDocument {
|
||||
id
|
||||
defaultApprovers {
|
||||
id
|
||||
}
|
||||
}
|
||||
...AssetsPageFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +50,6 @@ export const assetNodeQuery = graphql`
|
||||
node(id: $assetId) {
|
||||
... on Asset {
|
||||
id
|
||||
snapshotId
|
||||
name
|
||||
amount
|
||||
assetType
|
||||
@@ -75,7 +86,6 @@ export const createAssetMutation = graphql`
|
||||
assetEdge @appendEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
snapshotId
|
||||
name
|
||||
amount
|
||||
assetType
|
||||
@@ -107,7 +117,6 @@ export const updateAssetMutation = graphql`
|
||||
updateAsset(input: $input) {
|
||||
asset {
|
||||
id
|
||||
snapshotId
|
||||
name
|
||||
amount
|
||||
assetType
|
||||
@@ -146,8 +155,7 @@ export const useDeleteAsset = (
|
||||
asset: { id?: string; name?: string },
|
||||
connectionId: string,
|
||||
) => {
|
||||
// eslint-disable-next-line relay/generated-typescript-types
|
||||
const [mutate] = useMutation(deleteAssetMutation);
|
||||
const [mutate] = useMutation<AssetGraphDeleteMutation>(deleteAssetMutation);
|
||||
const confirm = useConfirm();
|
||||
const { __ } = useTranslate();
|
||||
|
||||
@@ -160,7 +168,7 @@ export const useDeleteAsset = (
|
||||
promisifyMutation(mutate)({
|
||||
variables: {
|
||||
input: {
|
||||
assetId: asset.id,
|
||||
assetId: asset.id!,
|
||||
},
|
||||
connections: [connectionId],
|
||||
},
|
||||
@@ -178,15 +186,14 @@ export const useDeleteAsset = (
|
||||
};
|
||||
|
||||
export const useCreateAsset = (connectionId: string) => {
|
||||
// eslint-disable-next-line relay/generated-typescript-types
|
||||
const [mutate, isMutating] = useMutation(createAssetMutation);
|
||||
const [mutate, isMutating] = useMutation<AssetGraphCreateMutation>(createAssetMutation);
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return [
|
||||
(input: {
|
||||
name: string;
|
||||
amount: number;
|
||||
assetType: string;
|
||||
assetType: AssetType;
|
||||
ownerId: string;
|
||||
organizationId: string;
|
||||
vendorIds?: string[];
|
||||
@@ -228,16 +235,13 @@ export const useCreateAsset = (connectionId: string) => {
|
||||
|
||||
export const useUpdateAsset = () => {
|
||||
const { __ } = useTranslate();
|
||||
const [mutate] = useMutationWithToasts(updateAssetMutation, {
|
||||
successMessage: __("Asset updated successfully"),
|
||||
errorMessage: __("Failed to update asset"),
|
||||
});
|
||||
const [mutate] = useMutation<AssetGraphUpdateMutation>(updateAssetMutation);
|
||||
|
||||
return (input: {
|
||||
id: string;
|
||||
name?: string;
|
||||
amount?: number;
|
||||
assetType?: string;
|
||||
assetType?: AssetType;
|
||||
dataTypesStored?: string;
|
||||
ownerId?: string;
|
||||
vendorIds?: string[];
|
||||
@@ -246,7 +250,7 @@ export const useUpdateAsset = () => {
|
||||
return alert(__("Failed to update asset: asset ID is required"));
|
||||
}
|
||||
|
||||
return mutate({
|
||||
return promisifyMutation(mutate)({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import {
|
||||
getAssetTypeVariant,
|
||||
validateSnapshotConsistency,
|
||||
} from "@probo/helpers";
|
||||
import { getAssetTypeVariant } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
@@ -32,14 +29,12 @@ import {
|
||||
type PreloadedQuery,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { AssetGraphNodeQuery } from "#/__generated__/core/AssetGraphNodeQuery.graphql";
|
||||
import { ControlledField } from "#/components/form/ControlledField";
|
||||
import { PeopleSelectField } from "#/components/form/PeopleSelectField";
|
||||
import { VendorsMultiSelectField } from "#/components/form/VendorsMultiSelectField";
|
||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
@@ -70,15 +65,10 @@ export default function AssetDetailsPage(props: Props) {
|
||||
const assetEntry = asset.node;
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
|
||||
validateSnapshotConsistency(assetEntry, snapshotId);
|
||||
|
||||
const connectionId = ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
"AssetsPage_assets",
|
||||
{ filter: { snapshotId: snapshotId || null } },
|
||||
);
|
||||
const deleteAsset = useDeleteAsset(assetEntry, connectionId);
|
||||
|
||||
@@ -107,25 +97,19 @@ export default function AssetDetailsPage(props: Props) {
|
||||
reset(formData);
|
||||
});
|
||||
|
||||
const breadcrumbAssetsUrl
|
||||
= isSnapshotMode && snapshotId
|
||||
? `/organizations/${organizationId}/snapshots/${snapshotId}/assets`
|
||||
: `/organizations/${organizationId}/assets`;
|
||||
const breadcrumbItems = [
|
||||
{
|
||||
label: __("Assets"),
|
||||
to: `/organizations/${organizationId}/assets`,
|
||||
},
|
||||
{
|
||||
label: assetEntry?.name ?? "",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
label: __("Assets"),
|
||||
to: breadcrumbAssetsUrl,
|
||||
},
|
||||
{
|
||||
label: assetEntry?.name ?? "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -138,7 +122,7 @@ export default function AssetDetailsPage(props: Props) {
|
||||
: __("Virtual")}
|
||||
</Badge>
|
||||
</div>
|
||||
{!isSnapshotMode && asset.node.canDelete && (
|
||||
{asset.node.canDelete && (
|
||||
<ActionDropdown variant="secondary">
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
@@ -156,14 +140,14 @@ export default function AssetDetailsPage(props: Props) {
|
||||
label={__("Name")}
|
||||
{...register("name")}
|
||||
type="text"
|
||||
disabled={isSnapshotMode}
|
||||
disabled={!assetEntry.canUpdate}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={__("Amount")}
|
||||
{...register("amount", { valueAsNumber: true })}
|
||||
type="number"
|
||||
disabled={isSnapshotMode}
|
||||
disabled={!assetEntry.canUpdate}
|
||||
/>
|
||||
|
||||
<ControlledField
|
||||
@@ -171,7 +155,7 @@ export default function AssetDetailsPage(props: Props) {
|
||||
name="assetType"
|
||||
type="select"
|
||||
label={__("Asset Type")}
|
||||
disabled={isSnapshotMode}
|
||||
disabled={!assetEntry.canUpdate}
|
||||
>
|
||||
<Option value="VIRTUAL">{__("Virtual")}</Option>
|
||||
<Option value="PHYSICAL">{__("Physical")}</Option>
|
||||
@@ -181,7 +165,7 @@ export default function AssetDetailsPage(props: Props) {
|
||||
label={__("Data Types Stored")}
|
||||
{...register("dataTypesStored")}
|
||||
type="text"
|
||||
disabled={isSnapshotMode}
|
||||
disabled={!assetEntry.canUpdate}
|
||||
/>
|
||||
|
||||
<PeopleSelectField
|
||||
@@ -189,7 +173,7 @@ export default function AssetDetailsPage(props: Props) {
|
||||
control={control}
|
||||
name="ownerId"
|
||||
label={__("Owner")}
|
||||
disabled={isSnapshotMode}
|
||||
disabled={!assetEntry.canUpdate}
|
||||
/>
|
||||
|
||||
<VendorsMultiSelectField
|
||||
@@ -198,11 +182,11 @@ export default function AssetDetailsPage(props: Props) {
|
||||
name="vendorIds"
|
||||
selectedVendors={vendors}
|
||||
label={__("Vendors")}
|
||||
disabled={isSnapshotMode}
|
||||
disabled={!assetEntry.canUpdate}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
{formState.isDirty && !isSnapshotMode && asset.node.canUpdate && (
|
||||
{formState.isDirty && assetEntry.canUpdate && (
|
||||
<Button type="submit" disabled={formState.isSubmitting}>
|
||||
{formState.isSubmitting ? __("Updating...") : __("Update")}
|
||||
</Button>
|
||||
|
||||
@@ -14,19 +14,24 @@
|
||||
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, IconPlusLarge, PageHeader } from "@probo/ui";
|
||||
import {
|
||||
Button,
|
||||
IconPageTextLine,
|
||||
IconPlusLarge,
|
||||
IconUpload,
|
||||
PageHeader,
|
||||
} from "@probo/ui";
|
||||
import {
|
||||
graphql,
|
||||
type PreloadedQuery,
|
||||
usePaginationFragment,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
|
||||
import type { AssetGraphListQuery } from "#/__generated__/core/AssetGraphListQuery.graphql";
|
||||
import type { AssetsListQuery } from "#/__generated__/core/AssetsListQuery.graphql";
|
||||
import type { AssetsPageFragment$key } from "#/__generated__/core/AssetsPageFragment.graphql";
|
||||
import { SnapshotBanner } from "#/components/SnapshotBanner";
|
||||
import { assetsQuery } from "#/hooks/graph/AssetGraph";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
@@ -34,6 +39,7 @@ import { AssetsTable } from "../../../components/assets/AssetsTable";
|
||||
import { ReadOnlyAssetsTable } from "../../../components/assets/ReadOnlyAssetsTable";
|
||||
|
||||
import { CreateAssetDialog } from "./dialogs/CreateAssetDialog";
|
||||
import { PublishAssetListDialog } from "./dialogs/PublishAssetListDialog";
|
||||
|
||||
const paginatedAssetsFragment = graphql`
|
||||
fragment AssetsPageFragment on Organization
|
||||
@@ -44,7 +50,6 @@ const paginatedAssetsFragment = graphql`
|
||||
after: { type: "CursorKey", defaultValue: null }
|
||||
before: { type: "CursorKey", defaultValue: null }
|
||||
last: { type: "Int", defaultValue: null }
|
||||
snapshotId: { type: "ID", defaultValue: null }
|
||||
) {
|
||||
assets(
|
||||
first: $first
|
||||
@@ -52,14 +57,12 @@ const paginatedAssetsFragment = graphql`
|
||||
last: $last
|
||||
before: $before
|
||||
orderBy: $orderBy
|
||||
filter: { snapshotId: $snapshotId }
|
||||
) @connection(key: "AssetsPage_assets", filters: ["filter"]) {
|
||||
) @connection(key: "AssetsPage_assets") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
id
|
||||
snapshotId
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
name
|
||||
# eslint-disable-next-line relay/unused-fields
|
||||
@@ -102,8 +105,7 @@ type Props = {
|
||||
export default function AssetsPage(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const { snapshotId } = useParams<{ snapshotId?: string }>();
|
||||
const isSnapshotMode = Boolean(snapshotId);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const data = usePreloadedQuery<AssetGraphListQuery>(
|
||||
assetsQuery,
|
||||
@@ -115,29 +117,56 @@ export default function AssetsPage(props: Props) {
|
||||
);
|
||||
const assets = pagination.data.assets?.edges.map(edge => edge.node);
|
||||
const connectionId = pagination.data.assets.__id;
|
||||
const defaultApproverIds = (data.node.assetListDocument?.defaultApprovers ?? []).map(a => a.id);
|
||||
|
||||
const canWrite = assets.some(asset => asset.canDelete || asset.canUpdate);
|
||||
usePageTitle(__("Assets"));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{snapshotId && <SnapshotBanner snapshotId={snapshotId} />}
|
||||
<PageHeader
|
||||
title={__("Assets")}
|
||||
description={__(
|
||||
"Manage your organization's assets and their classifications.",
|
||||
)}
|
||||
>
|
||||
{!isSnapshotMode && data.node.canCreateAsset && (
|
||||
<CreateAssetDialog
|
||||
connection={connectionId}
|
||||
organizationId={organizationId}
|
||||
>
|
||||
<Button icon={IconPlusLarge}>{__("Add asset")}</Button>
|
||||
</CreateAssetDialog>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
{data.node.assetListDocument?.id && (
|
||||
<Button variant="secondary" asChild>
|
||||
<Link
|
||||
to={`/organizations/${organizationId}/documents/${data.node.assetListDocument.id}`}
|
||||
>
|
||||
<IconPageTextLine size={16} />
|
||||
{__("Document")}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{data.node.canPublishAssets && (
|
||||
<PublishAssetListDialog
|
||||
organizationId={organizationId}
|
||||
defaultApproverIds={defaultApproverIds}
|
||||
onPublished={(documentId) => {
|
||||
void navigate(
|
||||
`/organizations/${organizationId}/documents/${documentId}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" icon={IconUpload}>
|
||||
{__("Publish")}
|
||||
</Button>
|
||||
</PublishAssetListDialog>
|
||||
)}
|
||||
{data.node.canCreateAsset && (
|
||||
<CreateAssetDialog
|
||||
connection={connectionId}
|
||||
organizationId={organizationId}
|
||||
>
|
||||
<Button icon={IconPlusLarge}>{__("Add asset")}</Button>
|
||||
</CreateAssetDialog>
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
{isSnapshotMode || !canWrite
|
||||
{!canWrite
|
||||
? (
|
||||
<ReadOnlyAssetsTable pagination={pagination} assets={assets} />
|
||||
)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
IconSend,
|
||||
IconUpload,
|
||||
useDialogRef,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { PublishAssetListDialogMutation } from "#/__generated__/core/PublishAssetListDialogMutation.graphql";
|
||||
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
|
||||
const publishMutation = graphql`
|
||||
mutation PublishAssetListDialogMutation(
|
||||
$input: PublishAssetListInput!
|
||||
) {
|
||||
publishAssetList(input: $input) {
|
||||
documentEdge {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface PublishAssetListDialogProps {
|
||||
children: ReactNode;
|
||||
organizationId: string;
|
||||
defaultApproverIds?: string[];
|
||||
onPublished?: (documentId: string) => void;
|
||||
}
|
||||
|
||||
export function PublishAssetListDialog({
|
||||
children,
|
||||
organizationId,
|
||||
defaultApproverIds,
|
||||
onPublished,
|
||||
}: PublishAssetListDialogProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const schema = useMemo(() => z.object({
|
||||
approverIds: z.array(z.string()),
|
||||
}), []);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
} = useFormWithSchema(schema, {
|
||||
defaultValues: {
|
||||
approverIds: defaultApproverIds ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const [publish, isPublishing]
|
||||
= useMutation<PublishAssetListDialogMutation>(publishMutation);
|
||||
|
||||
const approverIds = watch("approverIds");
|
||||
const hasApprovers = approverIds.length > 0;
|
||||
|
||||
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||
publish({
|
||||
variables: {
|
||||
input: {
|
||||
organizationId,
|
||||
approverIds: data.approverIds.length > 0 ? data.approverIds : undefined,
|
||||
},
|
||||
},
|
||||
onCompleted(response) {
|
||||
const documentId = response.publishAssetList?.documentEdge?.node?.id;
|
||||
if (documentId) {
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: hasApprovers
|
||||
? __("Approval requested successfully.")
|
||||
: __("Asset list published successfully."),
|
||||
variant: "success",
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
reset();
|
||||
onPublished?.(documentId);
|
||||
}
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to publish asset list"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="max-w-xl"
|
||||
ref={dialogRef}
|
||||
trigger={children}
|
||||
title={__("Publish Asset List")}
|
||||
>
|
||||
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||
<DialogContent padded>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{__("Select approvers to request approval before publishing, or publish directly without approvers.")}
|
||||
</p>
|
||||
<PeopleMultiSelectField
|
||||
name="approverIds"
|
||||
label={__("Approvers")}
|
||||
control={control}
|
||||
organizationId={organizationId}
|
||||
placeholder={__("Add approvers...")}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
icon={hasApprovers ? IconSend : IconUpload}
|
||||
disabled={isPublishing}
|
||||
>
|
||||
{hasApprovers ? __("Request approval") : __("Publish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -33,21 +33,7 @@ export const assetRoutes = [
|
||||
Fallback: PageSkeleton,
|
||||
loader: loaderFromQueryLoader(({ organizationId }) =>
|
||||
loadQuery<AssetGraphListQuery>(coreEnvironment, assetsQuery, {
|
||||
organizationId: organizationId,
|
||||
snapshotId: null,
|
||||
}),
|
||||
),
|
||||
Component: withQueryRef(
|
||||
lazy(() => import("#/pages/organizations/assets/AssetsPage")),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "snapshots/:snapshotId/assets",
|
||||
Fallback: PageSkeleton,
|
||||
loader: loaderFromQueryLoader(({ organizationId, snapshotId }) =>
|
||||
loadQuery<AssetGraphListQuery>(coreEnvironment, assetsQuery, {
|
||||
organizationId: organizationId,
|
||||
snapshotId: snapshotId,
|
||||
organizationId,
|
||||
}),
|
||||
),
|
||||
Component: withQueryRef(
|
||||
@@ -66,16 +52,4 @@ export const assetRoutes = [
|
||||
lazy(() => import("#/pages/organizations/assets/AssetDetailsPage")),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "snapshots/:snapshotId/assets/:assetId",
|
||||
Fallback: PageSkeleton,
|
||||
loader: loaderFromQueryLoader(({ assetId }) =>
|
||||
loadQuery<AssetGraphNodeQuery>(coreEnvironment, assetNodeQuery, {
|
||||
assetId,
|
||||
}),
|
||||
),
|
||||
Component: withQueryRef(
|
||||
lazy(() => import("#/pages/organizations/assets/AssetDetailsPage")),
|
||||
),
|
||||
},
|
||||
] satisfies AppRoute[];
|
||||
|
||||
Reference in New Issue
Block a user