Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 47 additions & 11 deletions .github/workflows/storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ jobs:
# output. The deploy jobs only run once this succeeds, so we never publish a
# Storybook built from assets that don't compile. Results are cached and
# restored by the parallel deploy jobs below.
#
# The cache is saved under a key unique to this run, and restored by the
# content-hash prefix. A fixed key let a run cancelled mid-save (by
# cancel-in-progress) hold the reservation, so the next run couldn't save,
# and its deploy jobs restored nothing and failed to find Storybook.
build-assets:
name: 'Build assets'
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
cache-key: ${{ steps.cache-key.outputs.value }}
cache-prefix: ${{ steps.cache-key.outputs.prefix }}
deploy-branch: ${{ steps.branch.outputs.value }}
steps:
- name: Checkout code
Expand All @@ -60,11 +66,15 @@ jobs:
# Keep this separate from frontend CI so deployment jobs do not race
# another workflow saving the same cache.
run: |
echo "value=storybook-v1-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'composer.lock', 'package.json', 'packages/*/package.json', 'packages/craftcms-ui/src/**', 'packages/craftcms-ui/tsconfig*.json', 'packages/craftcms-ui/vite.config.*', 'packages/craftcms-ui/tsdown.config.*', 'packages/craftcms-garnish/src/**', 'packages/craftcms-garnish/tsconfig*.json', 'packages/craftcms-garnish/vite.config.*', 'routes/**/*.php', 'src/**/*.php', 'workbench/**/*.php') }}" >> "$GITHUB_OUTPUT"
prefix="storybook-v1-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', 'composer.lock', 'package.json', 'packages/*/package.json', 'packages/craftcms-ui/src/**', 'packages/craftcms-ui/tsconfig*.json', 'packages/craftcms-ui/vite.config.*', 'packages/craftcms-ui/tsdown.config.*', 'packages/craftcms-garnish/src/**', 'packages/craftcms-garnish/tsconfig*.json', 'packages/craftcms-garnish/vite.config.*', 'routes/**/*.php', 'src/**/*.php', 'workbench/**/*.php') }}-"
echo "prefix=$prefix" >> "$GITHUB_OUTPUT"
echo "value=${prefix}${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT"

# Any earlier run with the same inputs will do; a match skips the
# install and build steps, and the save at the end.
- name: Restore install + build + generated cache
id: cache
uses: actions/cache@v4
uses: actions/cache/restore@v4
with:
path: |
node_modules
Expand All @@ -78,14 +88,15 @@ jobs:
resources/js/wayfinder
resources/js/generated
key: ${{ steps.cache-key.outputs.value }}
restore-keys: ${{ steps.cache-key.outputs.prefix }}

- parallel:
- name: Install Composer dependencies
if: steps.cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-matched-key == ''
run: composer install --no-interaction --no-progress

- name: Install frontend dependencies
if: steps.cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-matched-key == ''
# pnpm won't expand env vars in credentials coming from the project
# .npmrc, so the Font Awesome token goes in the user-level one.
run: |
Expand All @@ -95,27 +106,44 @@ jobs:
CRAFT_FONTAWESOME_TOKEN: ${{ secrets.CRAFT_FONTAWESOME_TOKEN }}

- name: Copy icons
if: steps.cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-matched-key == ''
run: composer copy-icons

# Build the garnish package so its `dist/*.d.ts` exist — `vue-tsc` resolves
# `@craftcms/garnish` from `dist` (its `types`), and dist is gitignored.
- name: Build @craftcms/garnish
if: steps.cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-matched-key == ''
run: vp run build:garnish

- name: Build @craftcms/ui
if: steps.cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-matched-key == ''
run: vp run build:ui

- name: Generate TypeScript types
if: steps.cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-matched-key == ''
run: vp run generate:types

- name: Generate Wayfinder routes
if: steps.cache.outputs.cache-hit != 'true'
if: steps.cache.outputs.cache-matched-key == ''
run: vp run generate:wayfinder

- name: Save install + build + generated cache
if: steps.cache.outputs.cache-matched-key == ''
uses: actions/cache/save@v4
with:
path: |
node_modules
packages/*/node_modules
packages/craftcms-ui/dist
packages/craftcms-garnish/dist
vendor
cms-assets/resources/icons
resources/js/actions
resources/js/routes
resources/js/wayfinder
resources/js/generated
key: ${{ steps.cache-key.outputs.value }}

- name: Determine deploy branch
id: branch
run: |
Expand Down Expand Up @@ -143,8 +171,10 @@ jobs:
cache: true
run-install: false

# This run's own cache, or the earlier one build-assets matched. A miss
# means nothing is installed, so fail here rather than at the build.
- name: Restore install + build + generated cache
uses: actions/cache@v4
uses: actions/cache/restore@v4
with:
path: |
node_modules
Expand All @@ -158,6 +188,8 @@ jobs:
resources/js/wayfinder
resources/js/generated
key: ${{ needs.build-assets.outputs.cache-key }}
restore-keys: ${{ needs.build-assets.outputs.cache-prefix }}
fail-on-cache-miss: true

- name: Build Storybook
run: vp run @craftcms/ui#build:storybook
Expand Down Expand Up @@ -190,8 +222,10 @@ jobs:
cache: true
run-install: false

# This run's own cache, or the earlier one build-assets matched. A miss
# means nothing is installed, so fail here rather than at the build.
- name: Restore install + build + generated cache
uses: actions/cache@v4
uses: actions/cache/restore@v4
with:
path: |
node_modules
Expand All @@ -205,6 +239,8 @@ jobs:
resources/js/wayfinder
resources/js/generated
key: ${{ needs.build-assets.outputs.cache-key }}
restore-keys: ${{ needs.build-assets.outputs.cache-prefix }}
fail-on-cache-miss: true

- name: Build Storybook
# build-assets already ran the transformer and Wayfinder, and their
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Source/.idea/*
Build/*
*.sass-cache
node_modules/
.pnpm-store/
vendor/
/bower_components
cpresources/*
Expand Down
3 changes: 2 additions & 1 deletion packages/craftcms-legacy/cp/src/js/ElementEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1928,7 +1928,8 @@ Craft.ElementEditor = Garnish.Base.extend(
}

if (this.isFullPage && !this.$contentContainer.length) {
this.$contentContainer = this.settings.$contentContainer ?? $('#content');
this.$contentContainer =
this.settings.$contentContainer ?? $('#content');
}

this.formHost = this.$contentContainer.find(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ export default class CraftSwitchButton extends LionSwitchButton {

:host([checked]) .switch-button__thumb:after {
--_checkmark-size: calc(var(--c-switch-thumb-height) / 2);
--_checkmark-offset: calc((var(--c-switch-thumb-height) - var(--_checkmark-size)) / 2 - 1px);
--_checkmark-offset: calc(
(var(--c-switch-thumb-height) - var(--_checkmark-size)) / 2 - 1px
);
content: '';
position: absolute;
inset-block-start: var(--_checkmark-offset);
Expand Down
134 changes: 118 additions & 16 deletions resources/js/modules/forms/AdminTableNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@
useTable,
} from '@tanstack/vue-table';
import {StorageSerializers, watchDebounced} from '@vueuse/core';
import {computed, defineComponent, h, onMounted, ref, watch} from 'vue';
import {
computed,
defineComponent,
h,
onMounted,
ref,
shallowRef,
watch,
} from 'vue';
import CraftInput from '@craftcms/ui/vue/CraftInput.vue';
import CraftSelectRich, {
type SelectRichOption,
} from '@craftcms/ui/vue/CraftSelectRich.vue';
import ActionMenu from '@/common/components/ActionMenu.vue';
import CpLink from '@/common/components/CpLink.vue';
import type {
ActionItemButton,
ActionItemLink,
CheckboxOption,
PaginationData,
Expand All @@ -30,6 +39,7 @@
} from '@/modules/elements/types/actions';
import LayoutSlot from '@/common/components/LayoutSlot.vue';
import {useLocalStorage} from '@/common/composables/useStorage';
import {openSlideout} from '@/common/slideouts';
import AdminTable from '@/modules/admin-table/components/AdminTable.vue';
import ElementStatus from '@/modules/elements/ElementStatus.vue';
import IndexViewSettings from '@/modules/elements/index/components/IndexViewSettings.vue';
Expand All @@ -44,6 +54,7 @@
type CraftTableFeatures,
} from '@/modules/admin-table/craftTable';
import AdminTableDeleteModal from './AdminTableDeleteModal.vue';
import FormModal from './FormModal.vue';
import type {FormNodePayload, FormValues} from './types';

interface TableColumn {
Expand All @@ -54,12 +65,22 @@

interface TableLink {
label: string;
url: string | null;
url?: string | null;
/** Opens the URL's screen in a slideout, reloading the table once it's saved. */
slideout?: boolean;
}

/** A menu item that opens a server-built form in a modal instead of following a link. */
interface TableModalAction {
label: string;
modalUrl: string;
actionUrl: string;
params?: FormValues;
}

interface TableMenu {
label: string;
items: TableLink[];
items: Array<TableLink | TableModalAction>;
}

interface TableIcon {
Expand Down Expand Up @@ -559,9 +580,37 @@
}

function renderLink(link: TableLink) {
return link.url
? h(CpLink, {href: link.url, inertia: false}, () => link.label)
: link.label;
if (!link.url) {
return link.label;
}

const url = link.url;

return h(
CpLink,
{
href: url,
inertia: false,
// A modified click still follows the link, to open the screen in a tab.
onClick: link.slideout
? (event: MouseEvent) => {
if (
event.altKey ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey ||
event.button !== 0
) {
return;
}

event.preventDefault();
void openSlideout(url, {onSaved: refreshTable});
}
: undefined,
},
() => link.label
);
}

function renderIcon(icon: TableIcon) {
Expand All @@ -572,25 +621,68 @@
return h('div', {innerHTML: cell.html});
}

function isModalAction(
item: TableLink | TableModalAction
): item is TableModalAction {
return 'modalUrl' in item && 'actionUrl' in item;
}

const menuModal = shallowRef<TableModalAction | null>(null);

function onMenuModalSubmitted(): void {
menuModal.value = null;
refreshTable();
}

function renderMenu(menu: TableMenu) {
return h(
ActionMenu,
{
label: menu.label,
actions: menu.items.map(
(item): ActionItemLink => ({
type: 'link',
href: item.url ?? '#',
label: item.label,
})
actions: menu.items.map((item): ActionItemButton | ActionItemLink =>
isModalAction(item)
? {
type: 'button',
label: item.label,
onClick: () => {
menuModal.value = item;
},
}
: {
type: 'link',
href: item.url ?? '#',
label: item.label,
}
),
},
{
invoker: () =>
// Without the slot `craft-action-menu` has no invoker, and files the button
// away with its content.
//
// The label reads as plain cell text until its row (`DataTable`'s
// `cp-table-row`) is hovered or focused, or its menu is open, so a column of
// these doesn't turn into a column of buttons. Where there's no hover to
// reveal it, the chevron stays put.
invoker: ({attributes}: {attributes: Record<string, string>}) =>
h(
'craft-button',
{type: 'button', size: 'small', variant: 'outline'},
menu.label
{
...attributes,
type: 'button',
size: 'small',
variant: 'plain',
flush: '',
class: 'group',
},
[
menu.label,
h('craft-icon', {
name: 'chevron-down',
slot: 'suffix',
class:
'transition-opacity [@media(hover:hover)]:opacity-0 [.cp-table-row:hover_&]:opacity-100 [.cp-table-row:focus-within_&]:opacity-100 group-aria-expanded:opacity-100',
}),
]
),
}
);
Expand Down Expand Up @@ -749,7 +841,9 @@
: [];
},
},
getRowId: (row) => String(row.id),
// Rows only need an `id` to be reordered or deleted, and rows sharing an id
// send `DataTable` into a render loop.
getRowId: (row, index) => String(row.id ?? `row-${index}`),
enableRowSelection: (row) =>
hasBulkFooter.value && row.original._deletable !== false,
onRowSelectionChange: (updater) => {
Expand Down Expand Up @@ -1130,5 +1224,13 @@
@close="deletingRow = null"
@deleted="onModalDeleted"
/>
<FormModal
v-if="menuModal"
:modal-url="menuModal.modalUrl"
:action-url="menuModal.actionUrl"
:params="menuModal.params"
@close="menuModal = null"
@submitted="onMenuModalSubmitted"
/>
</div>
</template>
Loading
Loading