diff --git a/content/actions/guides/deploying-to-amazon-elastic-container-service.md b/content/actions/guides/deploying-to-amazon-elastic-container-service.md
index 59b5b7bae2..023472a4cb 100644
--- a/content/actions/guides/deploying-to-amazon-elastic-container-service.md
+++ b/content/actions/guides/deploying-to-amazon-elastic-container-service.md
@@ -5,6 +5,7 @@ product: '{% data reusables.gated-features.actions %}'
versions:
free-pro-team: '*'
enterprise-server: '>=2.22'
+type: 'tutorial'
---
{% data reusables.actions.enterprise-beta %}
diff --git a/content/actions/guides/deploying-to-azure-app-service.md b/content/actions/guides/deploying-to-azure-app-service.md
index 3471b333ac..ef83e1b85a 100644
--- a/content/actions/guides/deploying-to-azure-app-service.md
+++ b/content/actions/guides/deploying-to-azure-app-service.md
@@ -5,6 +5,7 @@ product: '{% data reusables.gated-features.actions %}'
versions:
free-pro-team: '*'
enterprise-server: '>=2.22'
+type: 'tutorial'
---
{% data reusables.actions.enterprise-beta %}
@@ -54,7 +55,7 @@ Before creating your {% data variables.product.prodname_actions %} workflow, you
3. Configure an Azure publish profile and create an `AZURE_WEBAPP_PUBLISH_PROFILE` secret.
- Generate your Azure deployment credentials using a publish profile. For more information, see "[Generate deployment credentials](https://docs.microsoft.com/en-us/azure/app-service/deploy-github-actions?tabs=applevel#generate-deployment-credentials)" in the Azure documentation.
+ Generate your Azure deployment credentials using a publish profile. For more information, see "[Generate deployment credentials](https://docs.microsoft.com/en-us/azure/app-service/deploy-github-actions?tabs=applevel#generate-deployment-credentials)" in the Azure documentation.
In your {% data variables.product.prodname_dotcom %} repository, create a secret named `AZURE_WEBAPP_PUBLISH_PROFILE` that contains the contents of the publish profile. For more information on creating secrets, see "[Encrypted secrets](/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository)."
diff --git a/content/actions/guides/deploying-to-google-kubernetes-engine.md b/content/actions/guides/deploying-to-google-kubernetes-engine.md
index ed8256ebbf..325e0eedad 100644
--- a/content/actions/guides/deploying-to-google-kubernetes-engine.md
+++ b/content/actions/guides/deploying-to-google-kubernetes-engine.md
@@ -5,6 +5,7 @@ product: '{% data reusables.gated-features.actions %}'
versions:
free-pro-team: '*'
enterprise-server: '>=2.22'
+type: 'tutorial'
---
{% data reusables.actions.enterprise-beta %}
@@ -99,37 +100,37 @@ The following example workflow demonstrates how to build a container image and p
{% raw %}
```yaml{:copy}
name: Build and Deploy to GKE
-
+
on:
release:
types: [created]
-
+
env:
PROJECT_ID: ${{ secrets.GKE_PROJECT }}
GKE_CLUSTER: cluster-1 # Add your cluster name here.
GKE_ZONE: us-central1-c # Add your cluster zone here.
DEPLOYMENT_NAME: gke-test # Add your deployment name here.
IMAGE: static-site
-
+
jobs:
setup-build-publish-deploy:
name: Setup, Build, Publish, and Deploy
runs-on: ubuntu-latest
steps:
-
+
- name: Checkout
uses: actions/checkout@v2
-
+
# Setup gcloud CLI
- uses: google-github-actions/setup-gcloud@v0.2.0
with:
service_account_key: ${{ secrets.GKE_SA_KEY }}
project_id: ${{ secrets.GKE_PROJECT }}
-
+
# Configure docker to use the gcloud command-line tool as a credential helper
- run: |-
gcloud --quiet auth configure-docker
-
+
# Get the GKE credentials so we can deploy to the cluster
- uses: google-github-actions/get-gke-credentials@v0.2.1
with:
@@ -145,18 +146,18 @@ jobs:
--build-arg GITHUB_SHA="$GITHUB_SHA" \
--build-arg GITHUB_REF="$GITHUB_REF" \
.
-
+
# Push the Docker image to Google Container Registry
- name: Publish
run: |-
docker push "gcr.io/$PROJECT_ID/$IMAGE:$GITHUB_SHA"
-
+
# Set up kustomize
- name: Set up Kustomize
run: |-
curl -sfLo kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v3.1.0/kustomize_3.1.0_linux_amd64
chmod u+x ./kustomize
-
+
# Deploy the Docker image to the GKE cluster
- name: Deploy
run: |-
diff --git a/content/actions/index.md b/content/actions/index.md
index 593b3f462c..75e30f185f 100644
--- a/content/actions/index.md
+++ b/content/actions/index.md
@@ -36,7 +36,7 @@ changelog:
href: https://github.blog/changelog/2020-10-29-github-actions-ubuntu-latest-workflows-will-use-ubuntu-20-04
product_video: https://www.youtube-nocookie.com/embed/cP0I9w2coGU
-
+
redirect_from:
- /articles/automating-your-workflow-with-github-actions/
- /articles/customizing-your-project-with-github-actions/
@@ -72,7 +72,7 @@ versions:
{% render 'code-example-card' for actionsCodeExamples as example %}
-
+
{% octicon "search" width="24" %}
diff --git a/content/discussions/index.md b/content/discussions/index.md
index 66dca4f270..902d7f2a00 100644
--- a/content/discussions/index.md
+++ b/content/discussions/index.md
@@ -43,7 +43,7 @@ versions:
{% render 'discussions-community-card' for discussionsCommunityExamples as example %}
\ No newline at end of file
diff --git a/includes/liquid-tags/link-as-article-card.html b/includes/liquid-tags/link-as-article-card.html
new file mode 100644
index 0000000000..555cf1e0e2
--- /dev/null
+++ b/includes/liquid-tags/link-as-article-card.html
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/javascripts/filter-cards.js b/javascripts/filter-cards.js
index 986322b7ae..52280a2933 100644
--- a/javascripts/filter-cards.js
+++ b/javascripts/filter-cards.js
@@ -1,72 +1,104 @@
-function filterCards (cards, value) {
- const noResults = document.querySelector('.js-filter-card-no-results')
- const matchReg = new RegExp(value, 'i')
-
- // Track whether or not we had at least one match
- let hasMatches = false
-
- for (let index = 0; index < cards.length; index++) {
- const card = cards[index]
-
- // Filter was emptied
- if (!value) {
- // Make sure we don't show the "No results" blurb
- hasMatches = true
-
- // Hide all but the first 6
- if (index > 5) {
- card.classList.add('d-none')
- } else {
- card.classList.remove('d-none')
- }
-
- continue
- }
-
- // Check if this card matches - any `data-*` attribute contains the string
- const cardMatches = Object.keys(card.dataset)
- .some(key => matchReg.test(card.dataset[key]))
-
- if (cardMatches) {
- card.classList.remove('d-none')
- hasMatches = true
- } else {
- card.classList.add('d-none')
- }
- }
-
- // If there wasn't at least one match, show the "no results" text
- if (!hasMatches) {
- document.querySelector('.js-filter-card-value').textContent = value
- noResults.classList.remove('d-none')
- } else {
- noResults.classList.add('d-none')
- }
+function matchCardBySearch (card, searchString) {
+ const matchReg = new RegExp(searchString, 'i')
+ // Check if this card matches - any `data-*` attribute contains the string
+ return Object.keys(card.dataset).some(key => matchReg.test(card.dataset[key]))
}
-export default function filterCodeExamples () {
- const filter = document.querySelector('.js-filter-card-filter')
+function matchCardByAttribute (card, attribute, value) {
+ if (attribute in card.dataset) {
+ return card.dataset[attribute] === value
+ }
+ return false
+}
+
+export default function cardsFilter () {
+ const inputFilter = document.querySelector('.js-filter-card-filter')
+ const dropdownFilter = document.querySelector('.js-filter-card-filter-dropdown')
const cards = Array.from(document.querySelectorAll('.js-filter-card'))
const showMoreButton = document.querySelector('.js-filter-card-show-more')
+ const noResults = document.querySelector('.js-filter-card-no-results')
+ // if jsFilterCardMax not set, assume no limit (well, at 99)
+ const maxCards = showMoreButton ? parseInt(showMoreButton.dataset.jsFilterCardMax || 99) : null
- if (filter) {
- filter.addEventListener('keyup', evt => {
- const value = evt.currentTarget.value
+ const filterEventHandler = (evt) => {
+ const { currentTarget } = evt
+ const value = currentTarget.value
- // Show or hide the "Show more" button if there is a value
- if (value) showMoreButton.classList.add('d-none')
- else showMoreButton.classList.remove('d-none')
+ // Show or hide the "Show more" button if there is a value
+ if (value) {
+ showMoreButton.classList.add('d-none')
+ } else {
+ showMoreButton.classList.remove('d-none')
+ }
- filterCards(cards, value)
+ // Track whether or not we had at least one match
+ let hasMatches = false
+
+ for (let index = 0; index < cards.length; index++) {
+ const card = cards[index]
+
+ // Filter was emptied
+ if (!value) {
+ // Make sure we don't show the "No results" blurb
+ hasMatches = true
+
+ // Hide all but the first n number of cards
+ if (index > maxCards - 1) {
+ card.classList.add('d-none')
+ } else {
+ card.classList.remove('d-none')
+ }
+
+ continue
+ }
+
+ let cardMatches = false
+
+ if (currentTarget.tagName === 'INPUT') {
+ cardMatches = matchCardBySearch(card, value)
+ }
+
+ if (currentTarget.tagName === 'SELECT' && currentTarget.name) {
+ cardMatches = matchCardByAttribute(card, currentTarget.name, value)
+ }
+
+ if (cardMatches) {
+ card.classList.remove('d-none')
+ hasMatches = true
+ } else {
+ card.classList.add('d-none')
+ }
+ }
+
+ // If there wasn't at least one match, show the "no results" text
+ if (!hasMatches) {
+ noResults.classList.remove('d-none')
+ } else {
+ noResults.classList.add('d-none')
+ }
+
+ return hasMatches
+ }
+
+ if (inputFilter) {
+ inputFilter.addEventListener('keyup', (evt) => {
+ const hasMatches = filterEventHandler(evt)
+ if (!hasMatches) {
+ document.querySelector('.js-filter-card-value').textContent = evt.currentTarget.value
+ }
})
}
+ if (dropdownFilter) {
+ dropdownFilter.addEventListener('change', filterEventHandler)
+ }
+
if (showMoreButton) {
showMoreButton.addEventListener('click', evt => {
// Number of cards that are currently visible
const numShown = cards.filter(card => !card.classList.contains('d-none')).length
- // We want to show 6 more
- const totalToShow = numShown + 6
+ // We want to show n more cards
+ const totalToShow = numShown + maxCards
for (let index = numShown; index < cards.length; index++) {
const card = cards[index]
@@ -83,7 +115,7 @@ export default function filterCodeExamples () {
// They're all shown now, we should hide the button
if (totalToShow >= cards.length) {
- evt.currentTarget.style.display = 'none'
+ evt.currentTarget.classList.add('d-none')
}
})
}
diff --git a/javascripts/show-more.js b/javascripts/show-more.js
index 00a23bba81..d67ef4d362 100644
--- a/javascripts/show-more.js
+++ b/javascripts/show-more.js
@@ -1,5 +1,5 @@
/*
- * This utility makes it easy to implement a list of items, some of which are hidden initially
+ * This utility component implement a list of items, some of which are hidden initially
* until user clicks "show more".
*
* Example:
diff --git a/layouts/product-sublanding.html b/layouts/product-sublanding.html
index 92b67c4429..d9af6ab059 100644
--- a/layouts/product-sublanding.html
+++ b/layouts/product-sublanding.html
@@ -1,5 +1,5 @@
-{% assign guideTypes = site.data.ui.product_sublanding.guideTypes %}
+{% assign guideTypes = site.data.ui.product_sublanding.guide_types %}
{% include head %}
@@ -42,7 +42,7 @@
{{ forloop.index }}
-
{{ guideTypes[guide.type] }}
+
{{ guideTypes[guide.page.type] }}
{{ guide.title }}
{{ guide.intro }}
@@ -87,7 +87,7 @@
{{ forloop.index }}
{{ guide.title }}
-
{{ guideTypes[guide.type] }}
+
{{ guideTypes[guide.page.type] }}
{% endfor %}
@@ -106,9 +106,7 @@
-
-
-
+ {% include 'article-cards' %}
diff --git a/lib/get-link-data.js b/lib/get-link-data.js
index 0d7a078d0a..46a64d7730 100644
--- a/lib/get-link-data.js
+++ b/lib/get-link-data.js
@@ -5,7 +5,7 @@ const removeFPTFromPath = require('./remove-fpt-from-path')
// rawLinks is an array of paths: [ '/foo' ]
// we need to convert it to an array of localized objects: [ { href: '/en/foo', title: 'Foo', intro: 'Description here' } ]
-module.exports = async (rawLinks, context, additionalProperties = []) => {
+module.exports = async (rawLinks, context) => {
if (!rawLinks) return
const links = []
@@ -20,17 +20,11 @@ module.exports = async (rawLinks, context, additionalProperties = []) => {
const opts = { textOnly: true, encodeEntities: true }
- const props = {}
- for (const propName of additionalProperties) {
- props[propName] = linkedPage[propName]
- }
-
links.push({
href,
title: await linkedPage.renderTitle(context, opts),
intro: await linkedPage.renderProp('intro', context, opts),
- page: linkedPage,
- ...props
+ page: linkedPage
})
}
diff --git a/lib/liquid-tags/link-as-article-card.js b/lib/liquid-tags/link-as-article-card.js
new file mode 100644
index 0000000000..060e3989ee
--- /dev/null
+++ b/lib/liquid-tags/link-as-article-card.js
@@ -0,0 +1,10 @@
+const Link = require('./link')
+
+// For details, see class method in lib/liquid-tags/link.js
+module.exports = class LinkAsArticleCard extends Link {
+ async renderPageProps (page, ctx, props) {
+ const renderedProps = await super.renderPageProps(page, ctx, props)
+ const { type } = page
+ return { ...renderedProps, type }
+ }
+}
diff --git a/lib/liquid-tags/link.js b/lib/liquid-tags/link.js
index 5239dc98c5..d82449e2b0 100644
--- a/lib/liquid-tags/link.js
+++ b/lib/liquid-tags/link.js
@@ -5,6 +5,7 @@ const liquid = new Liquid.Engine()
const LiquidTag = require('./liquid-tag')
const findPage = require('../find-page')
const getApplicableVersions = require('../get-applicable-versions')
+const { getPathWithLanguage } = require('../path-utils')
// This class supports a set of link tags. Each tag expects one parameter, a language-agnostic href:
//
@@ -27,7 +28,18 @@ module.exports = class Link extends LiquidTag {
super(template, tagName, href.trim())
}
- async parseTemplate (context) {
+ async renderPageProps (page, ctx, props) {
+ const renderedProps = {}
+
+ for (const propName in props) {
+ const { opt } = props[propName] || {}
+ renderedProps[propName] = await page.renderProp(propName, ctx, opt)
+ }
+
+ return renderedProps
+ }
+
+ async parseTemplate (context, opts = { shortTitle: false }) {
const template = await this.getTemplate()
const ctx = context.environments[0]
@@ -38,7 +50,16 @@ module.exports = class Link extends LiquidTag {
assert(ctx.currentLanguage, 'context.currentLanguage is required')
// process any liquid in hrefs (e.g., /enterprise/{{ page.version }})
- const href = await liquid.parseAndRender(this.param, ctx)
+ let href = await liquid.parseAndRender(this.param, ctx)
+
+ // process variable defined in page scope
+ if (href === '') {
+ const match = liquidVariableSyntax.exec(this.param)
+ if (match) {
+ const variable = new Liquid.Variable(match[1])
+ href = await variable.render(context)
+ }
+ }
let fullPath = href
const dirName = path.dirname(ctx.page.relativePath)
@@ -52,7 +73,7 @@ module.exports = class Link extends LiquidTag {
}
// add language code
- fullPath = path.join('/', ctx.currentLanguage, fullPath)
+ fullPath = getPathWithLanguage(fullPath, ctx.currentLanguage)
// find the page based on the full path
const page = findPage(fullPath, ctx.pages, ctx.redirects)
@@ -61,20 +82,21 @@ module.exports = class Link extends LiquidTag {
if (!page || (page.hidden && !ctx.page.hidden)) {
return ''
}
-
// also return early if the found page should not render in current version
if (!getApplicableVersions(page.versions).includes(ctx.currentVersion)) {
return ''
}
// find and render the props
- const title = await page.renderProp('title', ctx, { textOnly: true, encodeEntities: true })
+ const renderedProps = await this.renderPageProps(page, ctx, {
+ title: { opt: { textOnly: true, encodeEntities: true } },
+ intro: { opt: { unwrap: true } }
+ })
- // we want markdown in intros to be parsed, so we do not pass textOnly here
- const intro = await page.renderProp('intro', ctx, { unwrap: true })
-
- const parsed = await liquid.parseAndRender(template, { fullPath, title, intro })
+ const parsed = await liquid.parseAndRender(template, { fullPath, ...renderedProps })
return parsed.trim()
}
}
+
+const liquidVariableSyntax = RegExp(`^${Liquid.VariableStart.source}\\s*(.*)\\s*${Liquid.VariableEnd.source}`)
diff --git a/lib/page.js b/lib/page.js
index 3e88491a0d..a7072b7bde 100644
--- a/lib/page.js
+++ b/lib/page.js
@@ -210,7 +210,7 @@ class Page {
learningTracks.push({
title: await renderContent(track.title, context, { textOnly: true, encodeEntities: true }),
description: await renderContent(track.description, context, { textOnly: true, encodeEntities: true }),
- guides: await getLinkData(track.guides, context, ['type'])
+ guides: await getLinkData(track.guides, context)
})
}
this.learningTracks = learningTracks
diff --git a/lib/render-content/index.js b/lib/render-content/index.js
index 20122bc1eb..3394225894 100644
--- a/lib/render-content/index.js
+++ b/lib/render-content/index.js
@@ -12,6 +12,7 @@ renderContent.liquid.registerTag('topic_link_in_list', require('../liquid-tags/t
renderContent.liquid.registerTag('indented_data_reference', require('../liquid-tags/indented-data-reference'))
renderContent.liquid.registerTag('data', require('../liquid-tags/data'))
renderContent.liquid.registerTag('octicon', require('../liquid-tags/octicon'))
+renderContent.liquid.registerTag('link_as_article_card', require('../liquid-tags/link-as-article-card'))
for (const tag in tags) {
// Register all the extended markdown tags, like {% note %} and {% warning %}
diff --git a/middleware/csp.js b/middleware/csp.js
index cbf6119f6e..bd5a3402c2 100644
--- a/middleware/csp.js
+++ b/middleware/csp.js
@@ -22,6 +22,7 @@ module.exports = async (req, res, next) => {
],
imgSrc: [
"'self'",
+ 'data:',
'github.githubassets.com',
'github-images.s3.amazonaws.com',
'octodex.github.com',
diff --git a/tests/browser/browser.js b/tests/browser/browser.js
index 597d301b56..956b52f216 100644
--- a/tests/browser/browser.js
+++ b/tests/browser/browser.js
@@ -249,3 +249,53 @@ describe('platform specific content', () => {
}
})
})
+
+describe('card filters', () => {
+ it('loads correctly', async () => {
+ await page.goto('http://localhost:4001/en/actions')
+ const shownCards = await page.$$('.js-filter-card:not(.d-none)')
+ const shownNoResult = await page.$('.js-filter-card-no-results:not(.d-none)')
+ const maxCards = await page.$eval('.js-filter-card-show-more', btn => parseInt(btn.dataset.jsFilterCardMax))
+ expect(shownCards.length).toBe(maxCards)
+ expect(shownNoResult).toBeNull()
+ })
+
+ it('filters cards', async () => {
+ await page.goto('http://localhost:4001/en/actions')
+ await page.click('input.js-filter-card-filter')
+ await page.type('input.js-filter-card-filter', 'issues')
+ const shownCards = await page.$$('.js-filter-card:not(.d-none)')
+ const showMoreClasses = await page.$eval('.js-filter-card-show-more', btn => Object.values(btn.classList))
+ expect(showMoreClasses).toContain('d-none')
+ expect(shownCards.length).toBeGreaterThan(1)
+ })
+
+ it('works with select input', async () => {
+ await page.goto('http://localhost:4001/en/actions/guides')
+ await page.select('.js-filter-card-filter-dropdown[name="type"]', 'overview')
+ const shownCards = await page.$$('.js-filter-card:not(.d-none)')
+ const shownCardsAttrib = await page.$$eval('.js-filter-card:not(.d-none)', cards =>
+ cards.map(card => card.dataset.type)
+ )
+ shownCardsAttrib.map(attrib => expect(attrib).toBe('overview'))
+ expect(shownCards.length).toBeGreaterThan(0)
+ })
+
+ it('shows more cards', async () => {
+ await page.goto('http://localhost:4001/en/actions')
+ const maxCards = await page.$eval('.js-filter-card-show-more', btn => parseInt(btn.dataset.jsFilterCardMax))
+ await page.click('.js-filter-card-show-more')
+ const shownCards = await page.$$('.js-filter-card:not(.d-none)')
+ expect(shownCards.length).toBe(maxCards * 2)
+ })
+
+ it('displays no result message', async () => {
+ await page.goto('http://localhost:4001/en/actions')
+ await page.click('input.js-filter-card-filter')
+ await page.type('input.js-filter-card-filter', 'this should not work')
+ const shownCards = await page.$$('.js-filter-card:not(.d-none)')
+ expect(shownCards.length).toBe(0)
+ const noResultsClasses = await page.$eval('.js-filter-card-no-results', elem => Object.values(elem.classList))
+ expect(noResultsClasses).not.toContain('d-none')
+ })
+})
diff --git a/tests/unit/liquid-helpers.js b/tests/unit/liquid-helpers.js
index fdf2073a7b..49509ce72b 100644
--- a/tests/unit/liquid-helpers.js
+++ b/tests/unit/liquid-helpers.js
@@ -45,6 +45,16 @@ describe('liquid helper tags', () => {
const expected = ''
const output = await liquid.parseAndRender(template, context)
expect(output.includes(expected)).toBe(true)
+ // set this back to english
+ context.currentLanguage = 'en'
+ })
+
+ test('link tag with local variable', async () => {
+ const template = `{% assign href = "/contributing-and-collaborating-using-github-desktop" %}
+ {% link {{ href }} %}`
+ const expected = ''
+ const output = await liquid.parseAndRender(template, context)
+ expect(output.includes(expected)).toBe(true)
})
test('link tag with absolute path', async () => {
@@ -84,6 +94,19 @@ describe('liquid helper tags', () => {
expect(output).toBe(expected)
})
+ test('link_as_article_card', async () => {
+ const template = '{% link_as_article_card /contributing-and-collaborating-using-github-desktop %}'
+ const expected = `