Skip to content

コピーボタンと参照コピーボタン ​

第5版作成 最終更新 (日本時間)
確認バージョン1.5.2.01.5.8.1

編集画面にはレコードを複製するボタンが 2 種類あります。このページでは両者の内部動作の違いと、複製時に引き継がれる値・リセットされる値をまとめます。

  • コピーはダイアログを経由して POST items/{id}/copy を送り、サーバー側ですぐに複製レコードを作成する
  • 参照コピーは ?CopyFrom={id} 付きの新規作成画面に遷移するだけ。コピー元の値が入力済みの状態で、ユーザーが手を加えてから「作成」で確定する
  • どちらも添付ファイル・コピー時の初期値を設定した列・読み取り権限のない列はリセットされ、リンクデータは CopyWithLinks で引き継がれる

2 つのボタンの違い ​

比較項目コピー参照コピー
ショートカットキーALT + cALT + k
操作後の流れコピーダイアログが開き、「コピー」でそのまま複製が作成される新規作成画面へ遷移し、コピー元の値が入力済みで表示される
有効にする設定AllowCopy(コピーを許可する)AllowReferenceCopy(参照コピーを許可する)
CopyFrom の受け渡しサーバー側で直接コピー元の ID を渡して複製URL パラメータ → 隠しフィールド → フォームの 2 段階
タイトルへの接尾辞CharToAddWhenCopying を自動で付ける付けない(ユーザーが編集する)
コメントダイアログの「コメントと共にコピー」で選べる(既定はオン)クリアされる
リンクデータCopyWithLinks で引き継ぐ同様に CopyWithLinks で引き継ぐ
添付ファイルSetCopyDefault() でリセット(引き継がない)同様にリセット

どちらのボタンも編集画面にだけ表示され、ログインユーザーに作成権限(CanCreate)が必要です。

編集画面下部の「コピー」「参照コピー」ボタン

設定画面の場所

設定は「テーブルの管理」の「エディタ」タブにある「コピーを許可」「参照コピーを許可」です。同じタブに、コピー時にタイトルへ追加する文字(CharToAddWhenCopying)の入力欄もあります(1.5.8.1 の SiteUtilities.cs の EditorSettingsEditor)。

テーブルの管理のエディタタブにある「コピーを許可」「参照コピーを許可」「コピー時に追加する文字」

コピーボタンの仕組み ​

ボタンとダイアログ ​

「コピー」ボタンは次のように定義されています。ss.IsSite(context) が true の場合は AllowCopy に関係なく表示されます。

cs
.Button(
    controlId: "OpenCopyDialogCommand",
    text: Displays.Copy(context: context),
    controlCss: "button-icon open-dialog button-positive",
    accessKey: "c",
    onClick: "$p.openDialog($(this));",
    icon: "ui-icon-copy",
    selector: "#CopyDialog",
    _using: copyButton
        && context.CanCreate(ss: ss)
        && (ss.IsSite(context: context) || ss.AllowCopy == true))

HtmlCommands.cs#L534-L546

クリックすると $p.openDialog($(this)) で #CopyDialog が開きます。ダイアログの構成は次のとおりです。

html
<div id="CopyDialog" class="dialog" title="コピー設定">
  <form id="CopyDialogForm" action="{ApplicationPath}items/{id}/_action_">
    <!-- チェックボックス: コメントと共にコピー(デフォルト: チェック) -->
    <input id="CopyWithComments" type="checkbox" class="always-send" checked>
    <!-- チェックボックス: 通知と共にコピー(設定によって表示) -->
    <input id="CopyWithNotifications" type="checkbox" class="always-send">
    <!-- チェックボックス: リマインダーと共にコピー(設定によって表示) -->
    <input id="CopyWithReminders" type="checkbox" class="always-send">

    <!-- コピー実行ボタン -->
    <button
      id="CopyCommand"
      onclick="$p.copy($(this));"
      data-action="Copy"
      data-method="post"
    >コピー</button>

    <!-- キャンセルボタン -->
    <button onclick="$p.closeDialog($(this));">キャンセル</button>
  </form>
</div>

コピーボタンで開くコピー設定ダイアログ(コメントをコピーする)

フォームの action の _action_ は送信時に data-action の値(Copy)で置き換えられ、送信先は {ApplicationPath}items/{id}/copy になります。ダイアログは HtmlCopies.cs で生成されています(HtmlCopies.cs#L10-L77)。

$p.copy() ​

ダイアログの「コピー」で呼ばれる $p.copy() は、フォームを同期送信し、成功したらブラウザの URL を更新するだけです。

js
$p.copy = function ($control) {
    var error = $p.syncSend($control);
    if (error === 0) {
        history.pushState(null, null, $('#BaseUrl').val() + $('#Id').val());
    }
};

item.js#L49-L54

送信データには次の項目が含まれます。

フィールド説明
ControlId"CopyCommand"(押したボタンの ID)
CopyWithCommentsチェック状態(true / false)
CopyWithNotificationsチェック状態(設定によって送信)
CopyWithRemindersチェック状態(設定によって送信)
TokenCSRF トークン

サーバー側の処理 ​

POST は ItemsController.Copy で受け取り、テーブル種別ごとの処理に委譲されます。

cs
[HttpPost]
public string Copy(long id)
{
    var context = new Context();
    var log = new SysLogModel(context: context);
    var json = new ItemModel(context: context, referenceId: id).Copy(context: context);
    log.Finish(context: context, responseSize: json.Length);
    return json;
}

ItemsController.cs#L771-L778

期限付きテーブル(Issues)の場合は次のとおりです。

cs
public static string Copy(Context context, SiteSettings ss, long issueId)
{
    // ...(権限チェックなど)...

    var issueModel = new IssueModel(
        context: context,
        ss: ss,
        issueId: issueId,
        formData: context.Forms);   // フォームデータ(チェックボックスの値など)を読み込む

    issueModel.IssueId = 0;         // 新規レコードとして扱う
    issueModel.Ver = 1;

    // タイトルにコピー時の追記文字を付加
    if (ss.GetEditorColumnNames().Contains("Title"))
    {
        issueModel.Title.Value += ss.CharToAddWhenCopying;
    }

    // CopyWithComments が false ならコメントをクリア
    if (!context.Forms.Bool("CopyWithComments"))
    {
        issueModel.Comments.Clear();
    }

    // サイト管理で設定したカラムのコピー時初期値を適用
    issueModel.SetCopyDefault(context: context, ss: ss);

    var errorData = issueModel.Create(
        context: context,
        ss: ss,
        copyFrom: issueId, ...);

    // ...(レスポンス生成)...
}

IssueUtilities.cs#L5589-L5658

処理内容
タイトル末尾への追記CharToAddWhenCopying(サイトで未設定なら General.json の値。1.5.8.1 の既定値は " - コピー"、General.json)を Title に追加。エディタにタイトル列がある場合のみ
コメントのクリアCopyWithComments が false のときコメントを空にする
コピー時初期値の適用SetCopyDefault()(後述)
作成Create() に copyFrom を渡し、リンクデータも複製する

コピー後の画面(SwitchRecordWithAjax) ​

コピー成功後の画面の切り替わり方は、サイト管理の「Ajax でレコードを切り替える」(SwitchRecordWithAjax)で変わります。

図を読み込み中…

設定動作
SwitchRecordWithAjax = true編集画面を AJAX で再描画し、ページ遷移なしでコピー後のレコードを表示
SwitchRecordWithAjax = false(既定値)コピー後のレコードの URL へ遷移

参照コピーボタンの仕組み ​

ボタンは新規作成画面へ遷移するだけ ​

参照コピーボタンの定義は次のとおりです。

cs
.Button(
    serverScriptModelRow: serverScriptModelRow,
    commandDisplayTypes: view?.ReferenceCopyCommand,
    controlId: "ReferenceCopyCommand",
    text: Displays.ReferenceCopy(context: context),
    controlCss: "button-icon button-positive",
    accessKey: "k",
    onClick: $"location.href='{Locations.ItemNew(context: context, id: ss.SiteId)}?CopyFrom={context.Id}'",
    icon: "ui-icon-copy",
    _using: copyButton
        && context.CanCreate(ss: ss)
        && !ss.IsSite(context: context)
        && ss.AllowReferenceCopy == true)

onClick は次の画面遷移です。ダイアログも API 呼び出しもありません。これが「参照コピーは新規作成とセットになる」理由です。

js
location.href = '/items/{siteId}/new?CopyFrom={currentItemId}'

表示されるのは次の条件をすべて満たすときです。

条件説明
copyButton呼び出し元で true が渡されている(編集画面が対象)
context.CanCreate(ss: ss)ログインユーザーが作成権限を持っている
!ss.IsSite(context: context)サイト設定画面ではない(通常のレコード編集画面である)
ss.AllowReferenceCopy参照コピーが有効になっている

CopyFrom は GET と POST の 2 回使われる ​

CopyFrom は、新規作成画面の表示(GET)と「作成」の保存(POST)の 2 回参照されます。

図を読み込み中…

GET:新規作成画面の初期値を用意する ​

新規作成画面(EditorNew)は、CopyFrom があればコピー元レコードを DB から取得し、CopyAndInit() で新規作成用に初期化します。

cs
public static string EditorNew(Context context, SiteSettings ss)
{
    // アイテム数上限チェック(超えていたらエラー画面を返す)
    if (context.ContractSettings.ItemsLimit(context: context, siteId: ss.SiteId))
    {
        return HtmlTemplates.Error(...);
    }

    IssueModel issueModel = null;
    var copyFrom = context.QueryStrings.Long("CopyFrom");

    if (ss.AllowReferenceCopy == true && copyFrom > 0)
    {
        // コピー元レコードを issueId = copyFrom で DB から SELECT
        issueModel = new IssueModel(
            context: context,
            ss: ss,
            issueId: copyFrom,
            methodType: BaseModel.MethodTypes.New);

        if (issueModel.AccessStatus == Databases.AccessStatuses.Selected
            && Permissions.CanRead(context: context, siteId: ss.SiteId, id: issueModel.IssueId))
        {
            // 読み取りOK → 値を複写して新規作成用に初期化
            issueModel = issueModel.CopyAndInit(context: context, ss: ss);
        }
        else
        {
            // 存在しない or 権限なし → NotFound エラー
            return HtmlTemplates.Error(context: context, errorData: new ErrorData(type: Error.Types.NotFound));
        }
    }

    // CopyFrom なし → 空の IssueModel で通常の新規作成画面
    return Editor(context: context, ss: ss,
        issueModel: issueModel ?? new IssueModel(context: context, ss: ss,
            methodType: BaseModel.MethodTypes.New, formData: context.Forms));
}

コピー元の読み取り権限が必要

コピー元レコードが存在しない、または読み取り権限がない場合は NotFound エラーになります。参照コピーは「コピー元を見る権限があること」が前提です。

CopyAndInit() は次の順に処理します。

cs
public IssueModel CopyAndInit(Context context, SiteSettings ss)
{
    // 空の新規モデルを作成
    var issueModel = new IssueModel(context: context, ss: ss, methodType: MethodTypes.New);

    // ① コピー元の全フィールドを新モデルに複写
    issueModel.SetByModel(this);

    // ② 新規作成に必要な値をリセット
    issueModel.IssueId = 0;      // ID なし(INSERT 後に採番される)
    issueModel.Ver = 1;           // バージョンは 1 に戻す
    issueModel.Comments = new Comments();  // コメントはクリア

    // ③ コピー時のデフォルト値を適用(CopyByDefault 列 / 添付 / 読み取り不可列)
    issueModel.SetCopyDefault(context: context, ss: ss);

    // ④ フォームデータを上書き(画面で変更済みの値があれば反映)
    issueModel.SetByForm(context: context, ss: ss, formData: context.Forms);

    // ⑤ サイト設定由来の値を適用(計算式・デフォルト値など)
    issueModel.SetBySettings(context: context, ss: ss, formData: context.Forms);

    // ⑥ ステータス制御(ステータス連動の項目を強制更新)
    issueModel.SetByStatusControls(context: context, ss: ss, force: true);

    return issueModel;
}

①の SetByModel() では、標準フィールド(タイトル・本文・開始日・完了日・進捗率・ステータス・担当者など)と拡張フィールド(ClassHash・NumHash・DateHash・DescriptionHash・CheckHash・AttachmentsHash)がすべてコピーされ、その後③でリセット対象の列が初期化されます。

GET 後:CopyFrom を隠しフィールドで引き継ぐ ​

新規作成画面には、CopyFrom の値が隠しフィールドとして埋め込まれます。always-send クラスが付いているため、「作成」を押したときの POST に自動で含まれます。

cs
.Hidden(
    controlId: "CopyFrom",
    css: "control-hidden always-send",
    value: context.QueryStrings.Long("CopyFrom").ToString(),
    _using: context.QueryStrings.Long("CopyFrom") > 0)

POST:権限を再確認し、リンクデータを引き継ぐ ​

「作成」を押すと Create が呼ばれ、ここでも CopyFrom が使われます。

cs
public static string Create(Context context, SiteSettings ss)
{
    var copyFrom = context.Forms.Int("CopyFrom");

    // コピー元への読み取り権限チェック(POST でも再検証)
    if (copyFrom > 0 && !Permissions.CanRead(context, siteId: context.SiteId, id: copyFrom))
    {
        return Error.Types.HasNotPermission.MessageJson(context: context);
    }

    var issueModel = new IssueModel(
        context, ss, issueId: copyFrom,
        setCopyDefault: copyFrom > 0,  // コピー元がある場合は CopyByDefault を適用
        formData: context.Forms);

    issueModel.IssueId = 0;
    issueModel.Ver = 1;

    // ... バリデーション・プロセス処理 ...

    if (copyFrom > 0)
    {
        // コピー元のコメントは引き継がない(新規作成中に追加したコメントのみ保持)
        issueModel.Comments.RemoveAll(o => !o.Created);
    }

    var errorData = issueModel.Create(
        context, ss,
        copyFrom: context.Forms.Long("CopyFrom"),  // IssueModel.Create に渡す
        otherInitValue: copyFrom > 0);

    // ...
}

IssueModel.Create() では、INSERT の完了後にリンクデータを複製します。

cs
if (copyFrom > 0)
{
    // リンク設定の "CopyWithLinks" アクションを実行
    // コピー元(From)のリンクデータを新レコード(To)へ複製する
    ss.LinkActions(
        context: context,
        type: "CopyWithLinks",
        data: new Dictionary<string, string>()
        {
            { "From", copyFrom.ToString() },
            { "To", IssueId.ToString() }
        });
}

コピー時にリセットされる値(SetCopyDefault) ​

コピー・参照コピーのどちらでも、SetCopyDefault() で次の列がデフォルト値に戻されます。

対象の列理由
CopyByDefault == true の列サイト設定でコピー時に初期化するよう設定されている(カラム設定の「コピー時の初期値」)
Attachments 型の列添付ファイルはコピーしない(別レコードに同じファイルを参照させない)
読み取り権限のない列ユーザーが参照できない値を引き継がせない
cs
public void SetCopyDefault(Context context, SiteSettings ss)
{
    ss.Columns
        .Where(column => column.CopyByDefault == true
            || column.TypeCs == "Attachments"
            || !column.CanRead(context: context, ss: ss, mine: Mine(context: context)))
        .ForEach(column => SetDefault(context: context, ss: ss, column: column));
}

「読み取り権限のない列」の判定には項目のアクセス制御が使われます。判定の仕組みは アクセス権限の実装 を参照してください。

応用:プログラムからコピーする $p.itemCopy ​

コピーボタンの仕組みを応用して、任意のレコードをスクリプトからコピーする $p.itemCopy を拡張スクリプトとして作れます。コピーボタンと同じく items/{id}/copy に POST します。

引数(args のプロパティ)型説明
idnumberコピー元のアイテム ID(省略時は現在のアイテム)
withCommentsbooleanコメントと共にコピーするか(既定: true)
donefunction(referenceId, json)成功時のコールバック。コピーされたアイテムの ReferenceId とレスポンス JSON を受け取る
failfunction(jqXHR)失敗時のコールバック
alwaysfunction()成否に関わらず実行されるコールバック

App_Data/Parameters/ExtendedScripts/ に ItemCopy.js として配置します。

js
/**
 * $p.itemCopy
 * 任意のアイテムをプログラムからコピーする拡張関数
 *
 * 使用例:
 *   $p.itemCopy({ done: function(referenceId) { console.log(referenceId); } });
 *   $p.itemCopy({ id: 12345, withComments: false, done: function(referenceId) { ... } });
 */
$p.itemCopy = function (args) {
    args = args || {};
    var id = args.id || $p.id();
    var data = {
        ControlId: 'CopyCommand',
        CopyWithComments: args.withComments !== false
    };
    if ($('#Token').length) {
        data.Token = $('#Token').val();
    }
    return $.ajax({
        type: 'post',
        url: $('#ApplicationPath').val() + 'items/' + id + '/copy',
        cache: false,
        data: data,
        dataType: 'json'
    }).done(function (json) {
        var resp = json.find(function (r) {
            return r.Method === 'Response' && r.Target === 'id';
        });
        var referenceId = resp ? Number(resp.Value) : null;
        if (typeof args.done === 'function') {
            args.done(referenceId, json);
        }
    }).fail(function (jqXHR) {
        if (typeof args.fail === 'function') {
            args.fail(jqXHR);
        }
    }).always(function () {
        if (typeof args.always === 'function') {
            args.always();
        }
    });
};

実装のポイントは次のとおりです。

ポイント内容
$p.apiExec と同じ構造$p.apiGet / $p.apiCreate などが内部で使う $p.apiExec と同じく、$.ajax の .done() / .fail() / .always() に args のコールバックを委ねる。書き方が $p.apiCreate などと揃う
新しい ID の取得コピー成功時のレスポンス JSON には { "Method": "Response", "Target": "id", "Value": "新しいID" } が含まれるので、これを find で取り出して数値にする
CSRF トークン$p.apiExec と同様に $('#Token').val() を送信データに加える

使い方の例 ​

js
// 現在のアイテムをコピーして新しい ID をコンソールに表示する
$p.itemCopy({
    done: function (referenceId) {
        console.log('コピー成功。新しいアイテム ID: ' + referenceId);
    },
    fail: function () {
        console.error('コピーに失敗しました。');
    }
});
js
// コメントなしでコピーし、新しいアイテムへ遷移する
$p.itemCopy({
    withComments: false,
    done: function (referenceId) {
        $p.transition($('#BaseUrl').val() + referenceId);
    }
});
js
// 特定のアイテムをコピーして新しい ID を取得する
$p.itemCopy({
    id: 12345,
    done: function (referenceId) {
        console.log('ID 12345 をコピー → 新 ID: ' + referenceId);
    },
    fail: function (jqXHR) {
        console.error('コピー失敗: ' + jqXHR.status);
    }
});

関連ページ ​

変更履歴

第5版「コピーボタンと参照コピーボタン」にスクリーンショットを追加
第4版「機能の仕様と使いこなし」を 1.5.8.1 のソースで検証して修正
第3版元記事への言及を整理し、必要なコードをページに収録。検索機能に一覧の検索と絞り込みを追加
第2版記事のファイル名に並び順の番号を付け、元記事リンクを frontmatter の sources に移行
第1版「機能の仕様と使いこなし」にコピーボタン・リンク項目の形式・拡張スタートガイドを追加